@hot-updater/firebase 1.0.0-rc.0 → 1.0.0-rc.2
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 +1255 -841
- package/dist/firebase/public/firestore.indexes.json +27 -37
- package/dist/iac/index.cjs +374 -173
- package/dist/iac/index.d.cts +1 -5
- package/dist/iac/index.d.mts +1 -5
- package/dist/iac/index.mjs +368 -167
- package/dist/index.cjs +183 -43
- package/dist/index.d.cts +2 -4
- package/dist/index.d.mts +1 -3
- package/dist/index.mjs +184 -44
- package/package.json +6 -6
|
@@ -6,7 +6,7 @@ let firebase_admin_firestore = require("firebase-admin/firestore");
|
|
|
6
6
|
let firebase_admin_storage = require("firebase-admin/storage");
|
|
7
7
|
//#endregion
|
|
8
8
|
//#region ../../packages/server/dist/version.mjs
|
|
9
|
-
const HOT_UPDATER_SERVER_VERSION = "1.0.0-rc.
|
|
9
|
+
const HOT_UPDATER_SERVER_VERSION = "1.0.0-rc.2";
|
|
10
10
|
//#endregion
|
|
11
11
|
//#region ../../packages/server/dist/handlerVersionRoutes.mjs
|
|
12
12
|
const createVersionRouteHandlers = () => ({ version: async () => Response.json({
|
|
@@ -14,244 +14,6 @@ const createVersionRouteHandlers = () => ({ version: async () => Response.json({
|
|
|
14
14
|
version: HOT_UPDATER_SERVER_VERSION
|
|
15
15
|
}) });
|
|
16
16
|
//#endregion
|
|
17
|
-
//#region ../../packages/server/dist/analytics/errors.mjs
|
|
18
|
-
var AnalyticsScanLimitExceededError = class extends Error {
|
|
19
|
-
constructor(limit) {
|
|
20
|
-
super(`Analytics event scan exceeded ${limit} rows.`);
|
|
21
|
-
this.limit = limit;
|
|
22
|
-
this.name = "AnalyticsScanLimitExceededError";
|
|
23
|
-
}
|
|
24
|
-
};
|
|
25
|
-
var AnalyticsBadRequestError = class extends Error {
|
|
26
|
-
name = "AnalyticsBadRequestError";
|
|
27
|
-
};
|
|
28
|
-
var AnalyticsPayloadTooLargeError = class extends Error {
|
|
29
|
-
name = "AnalyticsPayloadTooLargeError";
|
|
30
|
-
constructor(maximumBytes) {
|
|
31
|
-
super(`Event payload exceeds ${maximumBytes} bytes`);
|
|
32
|
-
this.maximumBytes = maximumBytes;
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
|
-
//#endregion
|
|
36
|
-
//#region ../../packages/server/dist/analytics/eventInput.mjs
|
|
37
|
-
const MAX_EVENT_STRING_LENGTH = 1024;
|
|
38
|
-
const EVENT_BODY_MAX_BYTES = 16 * 1024;
|
|
39
|
-
const eventKeys = new Set([
|
|
40
|
-
"type",
|
|
41
|
-
"installId",
|
|
42
|
-
"toBundleId",
|
|
43
|
-
"userId",
|
|
44
|
-
"username",
|
|
45
|
-
"platform",
|
|
46
|
-
"appVersion",
|
|
47
|
-
"channel",
|
|
48
|
-
"cohort",
|
|
49
|
-
"fingerprintHash",
|
|
50
|
-
"fromBundleId",
|
|
51
|
-
"fromReleaseId",
|
|
52
|
-
"toReleaseId",
|
|
53
|
-
"updateStrategy",
|
|
54
|
-
"sdkVersion"
|
|
55
|
-
]);
|
|
56
|
-
function isRecord$2(value) {
|
|
57
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
58
|
-
}
|
|
59
|
-
function requireStringField(payload, key) {
|
|
60
|
-
const value = payload[key];
|
|
61
|
-
if (typeof value !== "string" || value.length === 0 || value.length > MAX_EVENT_STRING_LENGTH) throw new AnalyticsBadRequestError(`Invalid event field: ${key}`);
|
|
62
|
-
return value;
|
|
63
|
-
}
|
|
64
|
-
function requireNullableStringField(payload, key) {
|
|
65
|
-
if (payload[key] === null) return null;
|
|
66
|
-
return requireStringField(payload, key);
|
|
67
|
-
}
|
|
68
|
-
async function readBoundedText(request) {
|
|
69
|
-
const contentLength = request.headers.get("content-length");
|
|
70
|
-
const declaredByteLength = Number(contentLength);
|
|
71
|
-
if (contentLength !== null && Number.isSafeInteger(declaredByteLength) && declaredByteLength > 16384) throw new AnalyticsPayloadTooLargeError(EVENT_BODY_MAX_BYTES);
|
|
72
|
-
if (request.body === null) return "";
|
|
73
|
-
const reader = request.body.getReader();
|
|
74
|
-
const decoder = new TextDecoder();
|
|
75
|
-
let byteLength = 0;
|
|
76
|
-
let text = "";
|
|
77
|
-
while (true) {
|
|
78
|
-
const result = await reader.read();
|
|
79
|
-
if (result.done) break;
|
|
80
|
-
byteLength += result.value.byteLength;
|
|
81
|
-
if (byteLength > 16384) {
|
|
82
|
-
await reader.cancel();
|
|
83
|
-
throw new AnalyticsPayloadTooLargeError(EVENT_BODY_MAX_BYTES);
|
|
84
|
-
}
|
|
85
|
-
text += decoder.decode(result.value, { stream: true });
|
|
86
|
-
}
|
|
87
|
-
return text + decoder.decode();
|
|
88
|
-
}
|
|
89
|
-
async function parseJson(request) {
|
|
90
|
-
const text = await readBoundedText(request);
|
|
91
|
-
try {
|
|
92
|
-
return JSON.parse(text);
|
|
93
|
-
} catch (error) {
|
|
94
|
-
if (error instanceof SyntaxError) throw new AnalyticsBadRequestError("Invalid event payload");
|
|
95
|
-
throw error;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
function requireEvent(payload) {
|
|
99
|
-
if (!isRecord$2(payload) || Object.keys(payload).some((key) => !eventKeys.has(key))) throw new AnalyticsBadRequestError("Invalid event payload");
|
|
100
|
-
const platform = requireStringField(payload, "platform");
|
|
101
|
-
if (platform !== "ios" && platform !== "android") throw new AnalyticsBadRequestError("Invalid event field: platform");
|
|
102
|
-
const base = {
|
|
103
|
-
installId: requireStringField(payload, "installId"),
|
|
104
|
-
toBundleId: requireStringField(payload, "toBundleId"),
|
|
105
|
-
...payload.userId === void 0 ? {} : { userId: requireStringField(payload, "userId") },
|
|
106
|
-
...payload.username === void 0 ? {} : { username: requireStringField(payload, "username") },
|
|
107
|
-
platform,
|
|
108
|
-
appVersion: requireStringField(payload, "appVersion"),
|
|
109
|
-
channel: requireStringField(payload, "channel"),
|
|
110
|
-
cohort: requireStringField(payload, "cohort"),
|
|
111
|
-
fingerprintHash: requireNullableStringField(payload, "fingerprintHash"),
|
|
112
|
-
sdkVersion: payload.sdkVersion === void 0 ? null : requireNullableStringField(payload, "sdkVersion"),
|
|
113
|
-
fromReleaseId: requireNullableStringField(payload, "fromReleaseId"),
|
|
114
|
-
toReleaseId: requireNullableStringField(payload, "toReleaseId")
|
|
115
|
-
};
|
|
116
|
-
const type = requireStringField(payload, "type");
|
|
117
|
-
switch (type) {
|
|
118
|
-
case "UPDATE_APPLIED":
|
|
119
|
-
case "RECOVERED":
|
|
120
|
-
case "RELEASE_ADOPTED": {
|
|
121
|
-
const updateStrategy = requireStringField(payload, "updateStrategy");
|
|
122
|
-
if (updateStrategy !== "fingerprint" && updateStrategy !== "appVersion") throw new AnalyticsBadRequestError("Invalid event field: updateStrategy");
|
|
123
|
-
return {
|
|
124
|
-
...base,
|
|
125
|
-
type,
|
|
126
|
-
fromBundleId: requireStringField(payload, "fromBundleId"),
|
|
127
|
-
updateStrategy
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
case "UNCHANGED":
|
|
131
|
-
if (payload.fromBundleId !== null || payload.updateStrategy !== null) throw new AnalyticsBadRequestError("Invalid unchanged event shape");
|
|
132
|
-
return {
|
|
133
|
-
...base,
|
|
134
|
-
type,
|
|
135
|
-
fromBundleId: null,
|
|
136
|
-
updateStrategy: null
|
|
137
|
-
};
|
|
138
|
-
default: throw new AnalyticsBadRequestError("Invalid event field: type");
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
async function parseBundleEventRequest(request) {
|
|
142
|
-
return requireEvent(await parseJson(request));
|
|
143
|
-
}
|
|
144
|
-
//#endregion
|
|
145
|
-
//#region ../../packages/server/dist/analytics/queryInput.mjs
|
|
146
|
-
const EVENT_LIST_BOUNDS = {
|
|
147
|
-
defaultValue: 50,
|
|
148
|
-
maximum: 100,
|
|
149
|
-
minimum: 1
|
|
150
|
-
};
|
|
151
|
-
const EVENT_LIST_OFFSET_BOUNDS = {
|
|
152
|
-
defaultValue: 0,
|
|
153
|
-
minimum: 0
|
|
154
|
-
};
|
|
155
|
-
const MAX_USER_ID_LENGTH = 1024;
|
|
156
|
-
function parseInteger(url, key, bounds) {
|
|
157
|
-
const value = url.searchParams.get(key);
|
|
158
|
-
if (value === null) return bounds.defaultValue;
|
|
159
|
-
const parsed = Number(value);
|
|
160
|
-
if (!Number.isSafeInteger(parsed) || parsed < bounds.minimum || bounds.maximum !== void 0 && parsed > bounds.maximum) throw new AnalyticsBadRequestError(`Invalid '${key}' query parameter.`);
|
|
161
|
-
return parsed;
|
|
162
|
-
}
|
|
163
|
-
const parsePagination = (request) => {
|
|
164
|
-
const url = new URL(request.url);
|
|
165
|
-
return {
|
|
166
|
-
limit: parseInteger(url, "limit", EVENT_LIST_BOUNDS),
|
|
167
|
-
offset: parseInteger(url, "offset", EVENT_LIST_OFFSET_BOUNDS)
|
|
168
|
-
};
|
|
169
|
-
};
|
|
170
|
-
const parseAnalyticsQuery = (request) => {
|
|
171
|
-
const window = new URL(request.url).searchParams.get("window") ?? "24h";
|
|
172
|
-
if (window !== "24h" && window !== "7d" && window !== "30d" && window !== "all") throw new AnalyticsBadRequestError("Invalid 'window' query parameter.");
|
|
173
|
-
return {
|
|
174
|
-
...parsePagination(request),
|
|
175
|
-
window
|
|
176
|
-
};
|
|
177
|
-
};
|
|
178
|
-
const parseActiveInstallationInput = (request) => {
|
|
179
|
-
const url = new URL(request.url);
|
|
180
|
-
const windows = url.searchParams.getAll("window");
|
|
181
|
-
if (windows.length > 1) throw new AnalyticsBadRequestError("Duplicate 'window' query parameter.");
|
|
182
|
-
const window = windows[0] ?? "30d";
|
|
183
|
-
if (window !== "24h" && window !== "7d" && window !== "30d") throw new AnalyticsBadRequestError("Invalid 'window' query parameter.");
|
|
184
|
-
const userIds = url.searchParams.getAll("userId");
|
|
185
|
-
if (userIds.length > 1) throw new AnalyticsBadRequestError("Duplicate 'userId' query parameter.");
|
|
186
|
-
const userId = userIds[0];
|
|
187
|
-
if (userId !== void 0 && (userId.length === 0 || userId.length > MAX_USER_ID_LENGTH)) throw new AnalyticsBadRequestError("Invalid 'userId' query parameter.");
|
|
188
|
-
return userId === void 0 ? { window } : {
|
|
189
|
-
window,
|
|
190
|
-
userId
|
|
191
|
-
};
|
|
192
|
-
};
|
|
193
|
-
const parseSearchInput = (request) => ({
|
|
194
|
-
...parsePagination(request),
|
|
195
|
-
query: new URL(request.url).searchParams.get("query")?.trim() ?? ""
|
|
196
|
-
});
|
|
197
|
-
//#endregion
|
|
198
|
-
//#region ../../packages/server/dist/analytics/routes.mjs
|
|
199
|
-
const json = (body, status) => Response.json(body, {
|
|
200
|
-
headers: { "cache-control": "private, no-store" },
|
|
201
|
-
status
|
|
202
|
-
});
|
|
203
|
-
const requireParam = (params, key) => {
|
|
204
|
-
const value = params[key];
|
|
205
|
-
if (value === void 0 || value.length === 0) throw new AnalyticsBadRequestError(`Missing route parameter: ${key}`);
|
|
206
|
-
return value;
|
|
207
|
-
};
|
|
208
|
-
const run = async (operation) => {
|
|
209
|
-
try {
|
|
210
|
-
return await operation();
|
|
211
|
-
} catch (error) {
|
|
212
|
-
if (error instanceof AnalyticsBadRequestError) return json({ error: error.message }, 400);
|
|
213
|
-
if (error instanceof AnalyticsPayloadTooLargeError) return json({ error: error.message }, 413);
|
|
214
|
-
if (error instanceof AnalyticsScanLimitExceededError) return json({ error: {
|
|
215
|
-
code: "ANALYTICS_SCAN_LIMIT_EXCEEDED",
|
|
216
|
-
limit: error.limit
|
|
217
|
-
} }, 503);
|
|
218
|
-
throw error;
|
|
219
|
-
}
|
|
220
|
-
};
|
|
221
|
-
const query = (operation) => run(async () => json(await operation(), 200));
|
|
222
|
-
const createAnalyticsRouteHandlers = (provider) => ({
|
|
223
|
-
appendBundleEvent: async (_params, request) => run(async () => {
|
|
224
|
-
await provider.appendBundleEvent(await parseBundleEventRequest(request));
|
|
225
|
-
return new Response(null, { status: 204 });
|
|
226
|
-
}),
|
|
227
|
-
getBundleEventSummary: (params) => query(() => provider.getBundleEventSummary(requireParam(params, "id"))),
|
|
228
|
-
getBundleEventAnalytics: (params, request) => query(() => {
|
|
229
|
-
const input = parseAnalyticsQuery(request);
|
|
230
|
-
return provider.getBundleEventAnalytics(requireParam(params, "id"), input.window, input.limit, input.offset);
|
|
231
|
-
}),
|
|
232
|
-
getBundleEventOverview: () => query(() => provider.getBundleEventOverview()),
|
|
233
|
-
getActiveInstallationOverview: (_params, request) => query(() => provider.getActiveInstallationOverview(parseActiveInstallationInput(request))),
|
|
234
|
-
searchInstallations: (_params, request) => query(() => {
|
|
235
|
-
const input = parseSearchInput(request);
|
|
236
|
-
return provider.searchInstallations(input.query, input.limit, input.offset);
|
|
237
|
-
}),
|
|
238
|
-
getInstallationHistory: (params, request) => query(() => {
|
|
239
|
-
const input = parsePagination(request);
|
|
240
|
-
return provider.getInstallationHistory(requireParam(params, "installId"), input.limit, input.offset);
|
|
241
|
-
})
|
|
242
|
-
});
|
|
243
|
-
const registerAnalyticsClientRoutes = (add) => {
|
|
244
|
-
add("POST", "/events", "appendBundleEvent");
|
|
245
|
-
};
|
|
246
|
-
const registerAnalyticsAdminRoutes = (add) => {
|
|
247
|
-
add("GET", "/bundles/:id/events/summary", "getBundleEventSummary");
|
|
248
|
-
add("GET", "/bundles/:id/events/analytics", "getBundleEventAnalytics");
|
|
249
|
-
add("GET", "/installations/overview", "getBundleEventOverview");
|
|
250
|
-
add("GET", "/installations/active", "getActiveInstallationOverview");
|
|
251
|
-
add("GET", "/installations", "searchInstallations");
|
|
252
|
-
add("GET", "/installations/:installId/events", "getInstallationHistory");
|
|
253
|
-
};
|
|
254
|
-
//#endregion
|
|
255
17
|
//#region ../../packages/server/dist/handlerErrors.mjs
|
|
256
18
|
var HandlerBadRequestError = class extends Error {
|
|
257
19
|
name = "HandlerBadRequestError";
|
|
@@ -577,6 +339,7 @@ const resolveManifestAssetStorageUri = ({ assetBaseStorageUri, assetPath, downlo
|
|
|
577
339
|
//#endregion
|
|
578
340
|
//#region ../plugin-core/dist/databasePluginCrudValidationErrors.mjs
|
|
579
341
|
var DatabasePluginInputError = class extends Error {
|
|
342
|
+
code;
|
|
580
343
|
name = "DatabasePluginInputError";
|
|
581
344
|
constructor(code) {
|
|
582
345
|
super(`Invalid database plugin input: ${code}`);
|
|
@@ -731,7 +494,7 @@ function getRolledOutNumericCohorts(bundleId, rolloutCohortCount) {
|
|
|
731
494
|
return Array.from({ length: normalizedRolloutCount }, (_, position) => positiveMod(multiplier * position + offset, NUMERIC_COHORT_SIZE)).map((zeroBasedCohort) => zeroBasedCohort + 1).sort((left, right) => left - right);
|
|
732
495
|
}
|
|
733
496
|
const RELEASE_CATALOG_FALLBACK_POLICY = "BUILTIN_IF_ACTIVE_INELIGIBLE";
|
|
734
|
-
const MAX_COMPILED_CATALOG_BYTES =
|
|
497
|
+
const MAX_COMPILED_CATALOG_BYTES = 262144;
|
|
735
498
|
const BASE64URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
736
499
|
const MAX_CATALOG_SEGMENT_LENGTH = 255;
|
|
737
500
|
function assertCatalogSegment(value, name) {
|
|
@@ -838,7 +601,7 @@ const isUUIDv7 = (value) => typeof value === "string" && UUID_V7_PATTERN.test(va
|
|
|
838
601
|
//#endregion
|
|
839
602
|
//#region ../plugin-core/dist/uuidv7.mjs
|
|
840
603
|
function createUUIDv7FromTimestampHex(timestampHex) {
|
|
841
|
-
const randomBytes = new Uint8Array(10);
|
|
604
|
+
const randomBytes = /* @__PURE__ */ new Uint8Array(10);
|
|
842
605
|
crypto.getRandomValues(randomBytes);
|
|
843
606
|
const randomHex = Array.from(randomBytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
844
607
|
const randA = randomHex.slice(0, 3);
|
|
@@ -935,6 +698,19 @@ const databaseFields = {
|
|
|
935
698
|
"sdk_version",
|
|
936
699
|
"received_at_ms"
|
|
937
700
|
],
|
|
701
|
+
bundle_installations: [
|
|
702
|
+
"id",
|
|
703
|
+
"install_id",
|
|
704
|
+
"user_id",
|
|
705
|
+
"username",
|
|
706
|
+
"to_bundle_id",
|
|
707
|
+
"type",
|
|
708
|
+
"platform",
|
|
709
|
+
"app_version",
|
|
710
|
+
"channel",
|
|
711
|
+
"cohort",
|
|
712
|
+
"received_at_ms"
|
|
713
|
+
],
|
|
938
714
|
api_keys: [
|
|
939
715
|
"id",
|
|
940
716
|
"hash",
|
|
@@ -947,7 +723,7 @@ const databaseFields = {
|
|
|
947
723
|
};
|
|
948
724
|
//#endregion
|
|
949
725
|
//#region ../plugin-core/dist/databasePluginCrudValidationFields.mjs
|
|
950
|
-
const isRecord$
|
|
726
|
+
const isRecord$3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
951
727
|
const isChannelText = (value) => {
|
|
952
728
|
if (typeof value !== "string" || value.length === 0) return false;
|
|
953
729
|
let codePointCount = 0;
|
|
@@ -957,6 +733,8 @@ const isChannelText = (value) => {
|
|
|
957
733
|
}
|
|
958
734
|
return true;
|
|
959
735
|
};
|
|
736
|
+
const isInsightsIdentityText = (value) => typeof value === "string" && value.length > 0 && value.length <= 255;
|
|
737
|
+
const isNullableInsightsIdentityText = (value) => value === null || isInsightsIdentityText(value);
|
|
960
738
|
const modelValidators = {
|
|
961
739
|
bundles: {
|
|
962
740
|
id: (value) => typeof value === "string",
|
|
@@ -1032,8 +810,8 @@ const modelValidators = {
|
|
|
1032
810
|
bundle_events: {
|
|
1033
811
|
id: (value) => typeof value === "string",
|
|
1034
812
|
type: (value) => value === "UPDATE_APPLIED" || value === "RECOVERED" || value === "RELEASE_ADOPTED" || value === "UNCHANGED",
|
|
1035
|
-
install_id:
|
|
1036
|
-
user_id:
|
|
813
|
+
install_id: isInsightsIdentityText,
|
|
814
|
+
user_id: isNullableInsightsIdentityText,
|
|
1037
815
|
username: (value) => value === null || typeof value === "string",
|
|
1038
816
|
from_bundle_id: (value) => value === null || typeof value === "string",
|
|
1039
817
|
from_release_id: (value) => value === null || typeof value === "string",
|
|
@@ -1048,6 +826,19 @@ const modelValidators = {
|
|
|
1048
826
|
sdk_version: (value) => value === null || typeof value === "string",
|
|
1049
827
|
received_at_ms: (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0
|
|
1050
828
|
},
|
|
829
|
+
bundle_installations: {
|
|
830
|
+
id: (value) => typeof value === "string",
|
|
831
|
+
install_id: isInsightsIdentityText,
|
|
832
|
+
user_id: isNullableInsightsIdentityText,
|
|
833
|
+
username: (value) => value === null || typeof value === "string",
|
|
834
|
+
to_bundle_id: (value) => typeof value === "string",
|
|
835
|
+
type: (value) => value === "UPDATE_APPLIED" || value === "RECOVERED" || value === "RELEASE_ADOPTED" || value === "UNCHANGED",
|
|
836
|
+
platform: (value) => value === "ios" || value === "android",
|
|
837
|
+
app_version: (value) => typeof value === "string",
|
|
838
|
+
channel: (value) => typeof value === "string",
|
|
839
|
+
cohort: (value) => typeof value === "string",
|
|
840
|
+
received_at_ms: (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0
|
|
841
|
+
},
|
|
1051
842
|
api_keys: {
|
|
1052
843
|
id: (value) => typeof value === "string",
|
|
1053
844
|
hash: (value) => typeof value === "string",
|
|
@@ -1058,7 +849,7 @@ const modelValidators = {
|
|
|
1058
849
|
revoked_at_ms: (value) => value === null || typeof value === "number" && Number.isSafeInteger(value) && value >= 0
|
|
1059
850
|
}
|
|
1060
851
|
};
|
|
1061
|
-
const stringFields = new Set([
|
|
852
|
+
const stringFields = /* @__PURE__ */ new Set([
|
|
1062
853
|
"id",
|
|
1063
854
|
"platform",
|
|
1064
855
|
"file_hash",
|
|
@@ -1099,7 +890,7 @@ const stringFields = new Set([
|
|
|
1099
890
|
"prefix",
|
|
1100
891
|
"role"
|
|
1101
892
|
]);
|
|
1102
|
-
const numberFields = new Set([
|
|
893
|
+
const numberFields = /* @__PURE__ */ new Set([
|
|
1103
894
|
"archive_byte_size",
|
|
1104
895
|
"byte_size",
|
|
1105
896
|
"rollout_cohort_count",
|
|
@@ -1112,13 +903,13 @@ const numberFields = new Set([
|
|
|
1112
903
|
"created_at_ms",
|
|
1113
904
|
"revoked_at_ms"
|
|
1114
905
|
]);
|
|
1115
|
-
const booleanFields = new Set([
|
|
906
|
+
const booleanFields = /* @__PURE__ */ new Set([
|
|
1116
907
|
"should_force_update",
|
|
1117
908
|
"enabled",
|
|
1118
909
|
"is_tombstone"
|
|
1119
910
|
]);
|
|
1120
911
|
const sortableFields = {
|
|
1121
|
-
bundles: new Set([
|
|
912
|
+
bundles: /* @__PURE__ */ new Set([
|
|
1122
913
|
"id",
|
|
1123
914
|
"platform",
|
|
1124
915
|
"file_hash",
|
|
@@ -1135,7 +926,7 @@ const sortableFields = {
|
|
|
1135
926
|
"manifest_file_hash",
|
|
1136
927
|
"asset_base_storage_uri"
|
|
1137
928
|
]),
|
|
1138
|
-
bundle_patches: new Set([
|
|
929
|
+
bundle_patches: /* @__PURE__ */ new Set([
|
|
1139
930
|
"id",
|
|
1140
931
|
"bundle_id",
|
|
1141
932
|
"base_bundle_id",
|
|
@@ -1145,7 +936,7 @@ const sortableFields = {
|
|
|
1145
936
|
"byte_size",
|
|
1146
937
|
"order_index"
|
|
1147
938
|
]),
|
|
1148
|
-
releases: new Set([
|
|
939
|
+
releases: /* @__PURE__ */ new Set([
|
|
1149
940
|
"id",
|
|
1150
941
|
"revision",
|
|
1151
942
|
"scope_key",
|
|
@@ -1165,7 +956,7 @@ const sortableFields = {
|
|
|
1165
956
|
"created_at_ms",
|
|
1166
957
|
"updated_at_ms"
|
|
1167
958
|
]),
|
|
1168
|
-
release_catalogs: new Set([
|
|
959
|
+
release_catalogs: /* @__PURE__ */ new Set([
|
|
1169
960
|
"scope_key",
|
|
1170
961
|
"catalog_id",
|
|
1171
962
|
"strategy",
|
|
@@ -1179,8 +970,8 @@ const sortableFields = {
|
|
|
1179
970
|
"is_tombstone",
|
|
1180
971
|
"updated_at_ms"
|
|
1181
972
|
]),
|
|
1182
|
-
channels: new Set(["id", "name"]),
|
|
1183
|
-
bundle_events: new Set([
|
|
973
|
+
channels: /* @__PURE__ */ new Set(["id", "name"]),
|
|
974
|
+
bundle_events: /* @__PURE__ */ new Set([
|
|
1184
975
|
"id",
|
|
1185
976
|
"type",
|
|
1186
977
|
"install_id",
|
|
@@ -1199,7 +990,20 @@ const sortableFields = {
|
|
|
1199
990
|
"sdk_version",
|
|
1200
991
|
"received_at_ms"
|
|
1201
992
|
]),
|
|
1202
|
-
|
|
993
|
+
bundle_installations: /* @__PURE__ */ new Set([
|
|
994
|
+
"id",
|
|
995
|
+
"install_id",
|
|
996
|
+
"user_id",
|
|
997
|
+
"username",
|
|
998
|
+
"to_bundle_id",
|
|
999
|
+
"type",
|
|
1000
|
+
"platform",
|
|
1001
|
+
"app_version",
|
|
1002
|
+
"channel",
|
|
1003
|
+
"cohort",
|
|
1004
|
+
"received_at_ms"
|
|
1005
|
+
]),
|
|
1006
|
+
api_keys: /* @__PURE__ */ new Set([
|
|
1203
1007
|
"id",
|
|
1204
1008
|
"hash",
|
|
1205
1009
|
"name",
|
|
@@ -1233,7 +1037,7 @@ const hasValidReleaseInvariants = (data) => {
|
|
|
1233
1037
|
};
|
|
1234
1038
|
const hasValidBundleEventInvariants = (data) => (data.type === "UPDATE_APPLIED" || data.type === "RECOVERED" || data.type === "RELEASE_ADOPTED") && typeof data.from_bundle_id === "string" && (data.update_strategy === "fingerprint" || data.update_strategy === "appVersion") || data.type === "UNCHANGED" && data.from_bundle_id === null && data.update_strategy === null;
|
|
1235
1039
|
const validateCreateData = (model, data) => {
|
|
1236
|
-
if (!isRecord$
|
|
1040
|
+
if (!isRecord$3(data)) throw new DatabasePluginInputError("invalid-data");
|
|
1237
1041
|
validateFields(model, Object.keys(data));
|
|
1238
1042
|
for (const field of databaseFields[model]) {
|
|
1239
1043
|
const validator = modelValidators[model][field];
|
|
@@ -1250,7 +1054,7 @@ const selectRow = (row, input) => {
|
|
|
1250
1054
|
return Object.fromEntries(select.map((field) => [field, Reflect.get(row, field)]));
|
|
1251
1055
|
};
|
|
1252
1056
|
const validateResult = (model, row, select) => {
|
|
1253
|
-
if (!isRecord$
|
|
1057
|
+
if (!isRecord$3(row)) throw new DatabasePluginInputError("invalid-result");
|
|
1254
1058
|
const fields = select ?? databaseFields[model];
|
|
1255
1059
|
for (const field of fields) {
|
|
1256
1060
|
const validator = modelValidators[model][field];
|
|
@@ -1271,17 +1075,182 @@ const validateResult = (model, row, select) => {
|
|
|
1271
1075
|
].every((field) => Object.hasOwn(row, field)) && !hasValidBundleEventInvariants(row)) throw new DatabasePluginInputError("invalid-result");
|
|
1272
1076
|
};
|
|
1273
1077
|
//#endregion
|
|
1078
|
+
//#region ../plugin-core/dist/insightsContract.mjs
|
|
1079
|
+
const encoder = new TextEncoder();
|
|
1080
|
+
/** Exact UTF-8 byte ordering, without case folding or Unicode normalization. */
|
|
1081
|
+
const compareInsightsText = (left, right) => {
|
|
1082
|
+
const a = encoder.encode(left);
|
|
1083
|
+
const b = encoder.encode(right);
|
|
1084
|
+
for (let index = 0; index < Math.min(a.length, b.length); index += 1) if (a[index] !== b[index]) return a[index] - b[index];
|
|
1085
|
+
return a.length - b.length;
|
|
1086
|
+
};
|
|
1087
|
+
const isWellFormedText = (value) => {
|
|
1088
|
+
for (const character of value) {
|
|
1089
|
+
const point = character.codePointAt(0);
|
|
1090
|
+
if (point >= 55296 && point <= 57343) return false;
|
|
1091
|
+
}
|
|
1092
|
+
return true;
|
|
1093
|
+
};
|
|
1094
|
+
const isIdentity = (value) => typeof value === "string" && value.length > 0 && value.length <= 255 && isWellFormedText(value);
|
|
1095
|
+
const isText = (value) => typeof value === "string" && value.length > 0 && isWellFormedText(value);
|
|
1096
|
+
const isTimestamp = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
1097
|
+
const isLimit = (value) => isTimestamp(value) && value >= 1 && value <= 101;
|
|
1098
|
+
const hasOnlyKeys$1 = (value, keys) => Object.keys(value).every((key) => keys.includes(key));
|
|
1099
|
+
const hasScope = (value) => (value.platform === "ios" || value.platform === "android") && isText(value.channel);
|
|
1100
|
+
const isBundleFilter = (value, withKind = false) => {
|
|
1101
|
+
if (!isRecord$3(value) || !hasScope(value)) return false;
|
|
1102
|
+
const keys = [
|
|
1103
|
+
"platform",
|
|
1104
|
+
"channel",
|
|
1105
|
+
"type",
|
|
1106
|
+
...withKind ? ["kind"] : []
|
|
1107
|
+
];
|
|
1108
|
+
return value.type === "RECOVERED" ? isText(value.fromBundleId) && hasOnlyKeys$1(value, [...keys, "fromBundleId"]) : (value.type === "UPDATE_APPLIED" || value.type === "RELEASE_ADOPTED") && isText(value.toBundleId) && hasOnlyKeys$1(value, [...keys, "toBundleId"]);
|
|
1109
|
+
};
|
|
1110
|
+
const isEventFilter = (value) => {
|
|
1111
|
+
if (!isRecord$3(value)) return false;
|
|
1112
|
+
if (value.kind === "all") return hasOnlyKeys$1(value, ["kind"]);
|
|
1113
|
+
if (value.kind === "installationMovement") return isIdentity(value.installId) && hasOnlyKeys$1(value, ["kind", "installId"]);
|
|
1114
|
+
return value.kind === "bundle" && isBundleFilter(value, true);
|
|
1115
|
+
};
|
|
1116
|
+
const validateRow = (model, row, result = false) => {
|
|
1117
|
+
try {
|
|
1118
|
+
validateCreateData(model, row);
|
|
1119
|
+
if (!isRecord$3(row) || typeof row.id !== "string" || !isUUIDv7(row.id) || Object.values(row).some((value) => typeof value === "string" && !isWellFormedText(value))) throw new DatabasePluginInputError("invalid-data");
|
|
1120
|
+
} catch (error) {
|
|
1121
|
+
if (result) throw new DatabasePluginInputError("invalid-result");
|
|
1122
|
+
throw error;
|
|
1123
|
+
}
|
|
1124
|
+
};
|
|
1125
|
+
/** Prepare the full latest-state candidate; the provider owns winning writes. */
|
|
1126
|
+
const toInsightsInstallationRow = (event) => {
|
|
1127
|
+
validateRow("bundle_events", event);
|
|
1128
|
+
return {
|
|
1129
|
+
id: event.id,
|
|
1130
|
+
install_id: event.install_id,
|
|
1131
|
+
user_id: event.user_id,
|
|
1132
|
+
username: event.username,
|
|
1133
|
+
to_bundle_id: event.to_bundle_id,
|
|
1134
|
+
type: event.type,
|
|
1135
|
+
platform: event.platform,
|
|
1136
|
+
app_version: event.app_version,
|
|
1137
|
+
channel: event.channel,
|
|
1138
|
+
cohort: event.cohort,
|
|
1139
|
+
received_at_ms: event.received_at_ms
|
|
1140
|
+
};
|
|
1141
|
+
};
|
|
1142
|
+
/** Release adoption and unchanged lifecycle reports are not bundle movements. */
|
|
1143
|
+
const isInsightsMovementEvent = (event) => event.type === "UPDATE_APPLIED" || event.type === "RECOVERED";
|
|
1144
|
+
const matchesInsightsEventFilter = (event, filter) => {
|
|
1145
|
+
if (filter.kind === "all") return true;
|
|
1146
|
+
if (filter.kind === "installationMovement") return event.install_id === filter.installId && isInsightsMovementEvent(event);
|
|
1147
|
+
return event.type === filter.type && event.platform === filter.platform && event.channel === filter.channel && (filter.type === "RECOVERED" ? event.from_bundle_id === filter.fromBundleId : event.to_bundle_id === filter.toBundleId);
|
|
1148
|
+
};
|
|
1149
|
+
const invalidQuery = () => {
|
|
1150
|
+
throw new DatabasePluginInputError("invalid-query");
|
|
1151
|
+
};
|
|
1152
|
+
const invalidResult = () => {
|
|
1153
|
+
throw new DatabasePluginInputError("invalid-result");
|
|
1154
|
+
};
|
|
1155
|
+
const validateCount = (count) => isTimestamp(count) ? count : invalidResult();
|
|
1156
|
+
/** Validate custom and bundled providers at the same public boundary. */
|
|
1157
|
+
const createValidatedInsightsModel = (model) => ({
|
|
1158
|
+
async record(input) {
|
|
1159
|
+
if (!isRecord$3(input) || !hasOnlyKeys$1(input, ["event", "installation"])) throw new DatabasePluginInputError("invalid-data");
|
|
1160
|
+
const expected = toInsightsInstallationRow(input.event);
|
|
1161
|
+
validateRow("bundle_installations", input.installation);
|
|
1162
|
+
if (Object.entries(expected).some(([field, value]) => Reflect.get(input.installation, field) !== value)) throw new DatabasePluginInputError("invalid-data");
|
|
1163
|
+
await model.record(input);
|
|
1164
|
+
},
|
|
1165
|
+
async listEvents(input) {
|
|
1166
|
+
if (!isRecord$3(input) || !hasOnlyKeys$1(input, [
|
|
1167
|
+
"filter",
|
|
1168
|
+
"sinceMs",
|
|
1169
|
+
"beforeReceivedAtMs",
|
|
1170
|
+
"after",
|
|
1171
|
+
"limit"
|
|
1172
|
+
]) || !isEventFilter(input.filter) || !isLimit(input.limit) || !isTimestamp(input.beforeReceivedAtMs) || input.sinceMs !== void 0 && !isTimestamp(input.sinceMs)) invalidQuery();
|
|
1173
|
+
const sinceMs = input.sinceMs ?? 0;
|
|
1174
|
+
if (sinceMs > input.beforeReceivedAtMs || input.after !== void 0 && (!isRecord$3(input.after) || !hasOnlyKeys$1(input.after, ["receivedAtMs", "id"]) || !isTimestamp(input.after.receivedAtMs) || typeof input.after.id !== "string" || !isUUIDv7(input.after.id) || input.after.receivedAtMs < sinceMs || input.after.receivedAtMs >= input.beforeReceivedAtMs)) invalidQuery();
|
|
1175
|
+
const rows = await model.listEvents(input);
|
|
1176
|
+
if (!Array.isArray(rows) || rows.length > input.limit) invalidResult();
|
|
1177
|
+
let previous = input.after;
|
|
1178
|
+
for (const row of rows) {
|
|
1179
|
+
validateRow("bundle_events", row, true);
|
|
1180
|
+
if (row.received_at_ms < sinceMs || row.received_at_ms >= input.beforeReceivedAtMs || !matchesInsightsEventFilter(row, input.filter) || previous !== void 0 && (row.received_at_ms > previous.receivedAtMs || row.received_at_ms === previous.receivedAtMs && row.id >= previous.id)) invalidResult();
|
|
1181
|
+
previous = {
|
|
1182
|
+
receivedAtMs: row.received_at_ms,
|
|
1183
|
+
id: row.id
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
return rows;
|
|
1187
|
+
},
|
|
1188
|
+
async findInstallations(input) {
|
|
1189
|
+
if (!isRecord$3(input)) invalidQuery();
|
|
1190
|
+
if ("installId" in input) {
|
|
1191
|
+
if (!hasOnlyKeys$1(input, ["installId"]) || !isIdentity(input.installId)) invalidQuery();
|
|
1192
|
+
} else if (!hasOnlyKeys$1(input, [
|
|
1193
|
+
"userId",
|
|
1194
|
+
"afterInstallId",
|
|
1195
|
+
"limit"
|
|
1196
|
+
]) || !isIdentity(input.userId) || !isLimit(input.limit) || input.afterInstallId !== void 0 && !isIdentity(input.afterInstallId)) invalidQuery();
|
|
1197
|
+
const rows = await model.findInstallations(input);
|
|
1198
|
+
if (!Array.isArray(rows) || rows.length > ("installId" in input ? 1 : input.limit)) invalidResult();
|
|
1199
|
+
let previous = "installId" in input ? void 0 : input.afterInstallId;
|
|
1200
|
+
for (const row of rows) {
|
|
1201
|
+
validateRow("bundle_installations", row, true);
|
|
1202
|
+
if ("installId" in input ? row.install_id !== input.installId : row.user_id !== input.userId || previous !== void 0 && compareInsightsText(row.install_id, previous) <= 0) invalidResult();
|
|
1203
|
+
previous = row.install_id;
|
|
1204
|
+
}
|
|
1205
|
+
return rows;
|
|
1206
|
+
},
|
|
1207
|
+
async countInstallations(input) {
|
|
1208
|
+
if (!isRecord$3(input) || !hasOnlyKeys$1(input, [
|
|
1209
|
+
"platform",
|
|
1210
|
+
"channel",
|
|
1211
|
+
"sinceMs",
|
|
1212
|
+
"bundleId"
|
|
1213
|
+
]) || !hasScope(input) || !isTimestamp(input.sinceMs) || input.bundleId !== void 0 && !isText(input.bundleId)) invalidQuery();
|
|
1214
|
+
return validateCount(await model.countInstallations(input));
|
|
1215
|
+
},
|
|
1216
|
+
async countEvents(input) {
|
|
1217
|
+
if (!isRecord$3(input) || !hasOnlyKeys$1(input, [
|
|
1218
|
+
"filter",
|
|
1219
|
+
"sinceMs",
|
|
1220
|
+
"beforeReceivedAtMs"
|
|
1221
|
+
]) || !isBundleFilter(input.filter) || !isTimestamp(input.sinceMs) || !isTimestamp(input.beforeReceivedAtMs) || input.sinceMs > input.beforeReceivedAtMs) invalidQuery();
|
|
1222
|
+
return validateCount(await model.countEvents(input));
|
|
1223
|
+
}
|
|
1224
|
+
});
|
|
1225
|
+
//#endregion
|
|
1274
1226
|
//#region ../plugin-core/dist/databasePluginCrudValidationMutations.mjs
|
|
1275
1227
|
const validateMutationWhere = (where) => {
|
|
1276
1228
|
if (where.length === 0) throw new DatabasePluginInputError("empty-mutation-where");
|
|
1277
1229
|
};
|
|
1278
1230
|
const validateUpdateWhere = (model, where) => {
|
|
1231
|
+
if (model === "bundle_installations") {
|
|
1232
|
+
const [install, receivedAt, id] = where;
|
|
1233
|
+
const exactInstall = isRecord$3(install) && install.field === "install_id" && (install.operator === void 0 || install.operator === "eq") && typeof install.value === "string" && install.connector === void 0 && install.mode === void 0;
|
|
1234
|
+
const receivedBefore = isRecord$3(receivedAt) && receivedAt.field === "received_at_ms" && (receivedAt.operator === "lt" || receivedAt.operator === "eq") && typeof receivedAt.value === "number" && Number.isSafeInteger(receivedAt.value) && receivedAt.value >= 0 && receivedAt.connector === void 0 && receivedAt.mode === void 0;
|
|
1235
|
+
const idBefore = isRecord$3(id) && id.field === "id" && id.operator === "lt" && typeof id.value === "string" && id.connector === void 0 && id.mode === void 0;
|
|
1236
|
+
if (!exactInstall || !receivedBefore || !(where.length === 2 && receivedAt.operator === "lt" || where.length === 3 && receivedAt.operator === "eq" && idBefore)) throw new DatabasePluginInputError("invalid-update-selector");
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1279
1239
|
const selector = where[0];
|
|
1280
1240
|
const primaryField = model === "release_catalogs" ? "scope_key" : "id";
|
|
1281
|
-
if (where.length !== 1 || !isRecord$
|
|
1241
|
+
if (where.length !== 1 || !isRecord$3(selector) || selector.field !== primaryField || selector.operator !== void 0 && selector.operator !== "eq" || typeof selector.value !== "string" || selector.connector !== void 0 || selector.mode !== void 0) throw new DatabasePluginInputError("invalid-update-selector");
|
|
1242
|
+
};
|
|
1243
|
+
const validateInsightsInstallationUpdateData = (update) => {
|
|
1244
|
+
if (!isRecord$3(update)) throw new DatabasePluginInputError("invalid-data");
|
|
1245
|
+
const fields = databaseFields.bundle_installations.filter((field) => field !== "install_id");
|
|
1246
|
+
if (Reflect.ownKeys(update).length !== fields.length || fields.some((field) => !Object.hasOwn(update, field))) throw new DatabasePluginInputError("invalid-data");
|
|
1247
|
+
for (const field of fields) {
|
|
1248
|
+
const validator = modelValidators.bundle_installations[field];
|
|
1249
|
+
if (!validator?.(Reflect.get(update, field))) throw new DatabasePluginInputError("invalid-data");
|
|
1250
|
+
}
|
|
1282
1251
|
};
|
|
1283
1252
|
const validateBundleUpdateData = (update) => {
|
|
1284
|
-
if (!isRecord$
|
|
1253
|
+
if (!isRecord$3(update)) throw new DatabasePluginInputError("invalid-data");
|
|
1285
1254
|
for (const [field, value] of Object.entries(update)) {
|
|
1286
1255
|
if (field === "id") throw new DatabasePluginInputError("invalid-data");
|
|
1287
1256
|
validateField("bundles", field);
|
|
@@ -1290,9 +1259,9 @@ const validateBundleUpdateData = (update) => {
|
|
|
1290
1259
|
}
|
|
1291
1260
|
};
|
|
1292
1261
|
const validateApiKeyUpdateData = (update) => {
|
|
1293
|
-
if (!isRecord$
|
|
1262
|
+
if (!isRecord$3(update) || Reflect.ownKeys(update).length !== 1 || !Object.hasOwn(update, "revoked_at_ms") || !modelValidators.api_keys.revoked_at_ms(Reflect.get(update, "revoked_at_ms"))) throw new DatabasePluginInputError("invalid-data");
|
|
1294
1263
|
};
|
|
1295
|
-
const RELEASE_MUTABLE_FIELDS = new Set([
|
|
1264
|
+
const RELEASE_MUTABLE_FIELDS = /* @__PURE__ */ new Set([
|
|
1296
1265
|
"revision",
|
|
1297
1266
|
"scope_key",
|
|
1298
1267
|
"target_app_version",
|
|
@@ -1305,7 +1274,7 @@ const RELEASE_MUTABLE_FIELDS = new Set([
|
|
|
1305
1274
|
"updated_at_ms"
|
|
1306
1275
|
]);
|
|
1307
1276
|
const validateReleaseUpdateData = (update) => {
|
|
1308
|
-
if (!isRecord$
|
|
1277
|
+
if (!isRecord$3(update) || Reflect.ownKeys(update).length === 0) throw new DatabasePluginInputError("invalid-data");
|
|
1309
1278
|
for (const [field, value] of Object.entries(update)) {
|
|
1310
1279
|
if (!RELEASE_MUTABLE_FIELDS.has(field)) throw new DatabasePluginInputError("invalid-data");
|
|
1311
1280
|
const validator = modelValidators.releases[field];
|
|
@@ -1313,7 +1282,7 @@ const validateReleaseUpdateData = (update) => {
|
|
|
1313
1282
|
}
|
|
1314
1283
|
};
|
|
1315
1284
|
const validateReleaseCatalogUpdateData = (update) => {
|
|
1316
|
-
if (!isRecord$
|
|
1285
|
+
if (!isRecord$3(update)) throw new DatabasePluginInputError("invalid-data");
|
|
1317
1286
|
const expectedFields = Object.keys(modelValidators.release_catalogs).filter((field) => field !== "scope_key");
|
|
1318
1287
|
if (Reflect.ownKeys(update).length !== expectedFields.length || expectedFields.some((field) => !Object.hasOwn(update, field))) throw new DatabasePluginInputError("invalid-data");
|
|
1319
1288
|
for (const [field, value] of Object.entries(update)) {
|
|
@@ -1393,7 +1362,7 @@ const validateWhere$1 = (model, where) => {
|
|
|
1393
1362
|
if (where === void 0) return;
|
|
1394
1363
|
if (!Array.isArray(where)) throw new DatabasePluginInputError("invalid-query");
|
|
1395
1364
|
for (const item of where) {
|
|
1396
|
-
if (!isRecord$
|
|
1365
|
+
if (!isRecord$3(item)) throw new DatabasePluginInputError("invalid-query");
|
|
1397
1366
|
if (item.connector !== void 0 && item.connector !== "AND" && item.connector !== "OR") throw new DatabasePluginInputError("invalid-query");
|
|
1398
1367
|
validateWhereValue(model, item);
|
|
1399
1368
|
}
|
|
@@ -1410,7 +1379,7 @@ const validateOrderBy = (model, orderBy) => {
|
|
|
1410
1379
|
if (!Array.isArray(orderBy) || orderBy.length === 0) throw new DatabasePluginInputError("invalid-query");
|
|
1411
1380
|
const fields = /* @__PURE__ */ new Set();
|
|
1412
1381
|
return orderBy.map((clause) => {
|
|
1413
|
-
if (!isRecord$
|
|
1382
|
+
if (!isRecord$3(clause) || typeof clause.field !== "string") throw new DatabasePluginInputError("invalid-query");
|
|
1414
1383
|
validateField(model, clause.field);
|
|
1415
1384
|
if (!sortableFields[model].has(clause.field)) throw new DatabasePluginInputError("invalid-query");
|
|
1416
1385
|
if (clause.direction !== "asc" && clause.direction !== "desc") throw new DatabasePluginInputError("invalid-query");
|
|
@@ -1422,7 +1391,7 @@ const validateOrderBy = (model, orderBy) => {
|
|
|
1422
1391
|
};
|
|
1423
1392
|
const validateDistinctOn = (model, distinctOn, orderBy) => {
|
|
1424
1393
|
if (distinctOn === void 0) return;
|
|
1425
|
-
if (!isRecord$
|
|
1394
|
+
if (!isRecord$3(distinctOn)) throw new DatabasePluginInputError("invalid-distinct");
|
|
1426
1395
|
const fields = validateDistinctFields(model, distinctOn.fields);
|
|
1427
1396
|
if (fields === void 0 || orderBy === void 0) throw new DatabasePluginInputError("invalid-distinct");
|
|
1428
1397
|
for (const [index, field] of fields.entries()) if (orderBy[index]?.field !== field) throw new DatabasePluginInputError("invalid-distinct");
|
|
@@ -1436,7 +1405,7 @@ const validateBundlePagination = (options) => {
|
|
|
1436
1405
|
if (!Number.isSafeInteger(options.limit) || options.limit <= 0 || options.page !== void 0 && (!Number.isSafeInteger(options.page) || options.page <= 0) || options.page !== void 0 && options.cursor !== void 0) throw new DatabasePluginInputError("invalid-pagination");
|
|
1437
1406
|
const cursor = options.cursor;
|
|
1438
1407
|
if (cursor === void 0) return;
|
|
1439
|
-
if (!isRecord$
|
|
1408
|
+
if (!isRecord$3(cursor)) throw new DatabasePluginInputError("invalid-pagination");
|
|
1440
1409
|
const hasAfter = Object.hasOwn(cursor, "after");
|
|
1441
1410
|
if (hasAfter === Object.hasOwn(cursor, "before")) throw new DatabasePluginInputError("invalid-pagination");
|
|
1442
1411
|
const value = hasAfter ? cursor.after : cursor.before;
|
|
@@ -1448,7 +1417,7 @@ const createDatabasePluginCrud = (implementation) => {
|
|
|
1448
1417
|
async function create(input) {
|
|
1449
1418
|
validateModel(input.model);
|
|
1450
1419
|
validateCreateData(input.model, input.data);
|
|
1451
|
-
if (input.onConflict !== void 0 && !(input.onConflict === "ignore" && (input.model === "channels" || input.model === "api_keys"))) throw new DatabasePluginInputError("invalid-operation");
|
|
1420
|
+
if (input.onConflict !== void 0 && !(input.onConflict === "ignore" && (input.model === "channels" || input.model === "api_keys" || input.model === "bundle_installations"))) throw new DatabasePluginInputError("invalid-operation");
|
|
1452
1421
|
validateSelect(input.model, input.select);
|
|
1453
1422
|
const row = await implementation.create(input);
|
|
1454
1423
|
validateResult(input.model, row, input.select);
|
|
@@ -1462,6 +1431,7 @@ const createDatabasePluginCrud = (implementation) => {
|
|
|
1462
1431
|
if (input.model === "bundles") validateBundleUpdateData(input.update);
|
|
1463
1432
|
else if (input.model === "releases") validateReleaseUpdateData(input.update);
|
|
1464
1433
|
else if (input.model === "release_catalogs") validateReleaseCatalogUpdateData(input.update);
|
|
1434
|
+
else if (input.model === "bundle_installations") validateInsightsInstallationUpdateData(input.update);
|
|
1465
1435
|
else if (input.model === "api_keys") validateApiKeyUpdateData(input.update);
|
|
1466
1436
|
else throw new DatabasePluginInputError("invalid-operation");
|
|
1467
1437
|
validateSelect(input.model, input.select);
|
|
@@ -1531,7 +1501,87 @@ const createTransactionDatabasePlugin = (implementation) => {
|
|
|
1531
1501
|
//#region ../plugin-core/dist/createDatabasePlugin.mjs
|
|
1532
1502
|
const PAGE_SIZE$1 = 100;
|
|
1533
1503
|
const compareChannelRows = (left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0;
|
|
1504
|
+
const toInsightsBundleWhere = (filter) => [
|
|
1505
|
+
{
|
|
1506
|
+
field: "platform",
|
|
1507
|
+
value: filter.platform
|
|
1508
|
+
},
|
|
1509
|
+
{
|
|
1510
|
+
field: "channel",
|
|
1511
|
+
value: filter.channel
|
|
1512
|
+
},
|
|
1513
|
+
{
|
|
1514
|
+
field: "type",
|
|
1515
|
+
value: filter.type
|
|
1516
|
+
},
|
|
1517
|
+
filter.type === "RECOVERED" ? {
|
|
1518
|
+
field: "from_bundle_id",
|
|
1519
|
+
value: filter.fromBundleId
|
|
1520
|
+
} : {
|
|
1521
|
+
field: "to_bundle_id",
|
|
1522
|
+
value: filter.toBundleId
|
|
1523
|
+
}
|
|
1524
|
+
];
|
|
1525
|
+
const toInsightsEventRanges = (filter) => {
|
|
1526
|
+
if (filter.kind === "all") return [[]];
|
|
1527
|
+
if (filter.kind === "bundle") return [toInsightsBundleWhere(filter)];
|
|
1528
|
+
return ["UPDATE_APPLIED", "RECOVERED"].map((type) => [{
|
|
1529
|
+
field: "install_id",
|
|
1530
|
+
value: filter.installId
|
|
1531
|
+
}, {
|
|
1532
|
+
field: "type",
|
|
1533
|
+
value: type
|
|
1534
|
+
}]);
|
|
1535
|
+
};
|
|
1536
|
+
const listInsightsEventRange = async (crud, input, filterWhere) => {
|
|
1537
|
+
const where = [...filterWhere, {
|
|
1538
|
+
field: "received_at_ms",
|
|
1539
|
+
operator: "gte",
|
|
1540
|
+
value: input.sinceMs ?? 0
|
|
1541
|
+
}];
|
|
1542
|
+
const sameTimestamp = input.after === void 0 ? [] : await crud.findMany({
|
|
1543
|
+
model: "bundle_events",
|
|
1544
|
+
where: [
|
|
1545
|
+
...filterWhere,
|
|
1546
|
+
{
|
|
1547
|
+
field: "received_at_ms",
|
|
1548
|
+
value: input.after.receivedAtMs
|
|
1549
|
+
},
|
|
1550
|
+
{
|
|
1551
|
+
field: "id",
|
|
1552
|
+
operator: "lt",
|
|
1553
|
+
value: input.after.id
|
|
1554
|
+
}
|
|
1555
|
+
],
|
|
1556
|
+
orderBy: [{
|
|
1557
|
+
field: "id",
|
|
1558
|
+
direction: "desc"
|
|
1559
|
+
}],
|
|
1560
|
+
limit: input.limit,
|
|
1561
|
+
offset: 0
|
|
1562
|
+
});
|
|
1563
|
+
if (sameTimestamp.length === input.limit) return sameTimestamp;
|
|
1564
|
+
const older = await crud.findMany({
|
|
1565
|
+
model: "bundle_events",
|
|
1566
|
+
where: [...where, {
|
|
1567
|
+
field: "received_at_ms",
|
|
1568
|
+
operator: "lt",
|
|
1569
|
+
value: input.after?.receivedAtMs ?? input.beforeReceivedAtMs
|
|
1570
|
+
}],
|
|
1571
|
+
orderBy: [{
|
|
1572
|
+
field: "received_at_ms",
|
|
1573
|
+
direction: "desc"
|
|
1574
|
+
}, {
|
|
1575
|
+
field: "id",
|
|
1576
|
+
direction: "desc"
|
|
1577
|
+
}],
|
|
1578
|
+
limit: input.limit - sameTimestamp.length,
|
|
1579
|
+
offset: 0
|
|
1580
|
+
});
|
|
1581
|
+
return [...sameTimestamp, ...older];
|
|
1582
|
+
};
|
|
1534
1583
|
var DatabaseAtomicCommitUnsupportedError = class extends Error {
|
|
1584
|
+
pluginName;
|
|
1535
1585
|
name = "DatabaseAtomicCommitUnsupportedError";
|
|
1536
1586
|
constructor(pluginName) {
|
|
1537
1587
|
super(`Database plugin "${pluginName}" cannot atomically commit changes across models.`);
|
|
@@ -1552,6 +1602,7 @@ var DatabaseRowReferencedError = class extends Error {
|
|
|
1552
1602
|
}
|
|
1553
1603
|
};
|
|
1554
1604
|
var DatabaseCommitConflictError = class extends Error {
|
|
1605
|
+
result;
|
|
1555
1606
|
name = "DatabaseCommitConflictError";
|
|
1556
1607
|
constructor(result) {
|
|
1557
1608
|
super("Database commit precondition failed.");
|
|
@@ -1779,12 +1830,6 @@ const applyChange = async (database, change, changeIndex) => {
|
|
|
1779
1830
|
}
|
|
1780
1831
|
return;
|
|
1781
1832
|
}
|
|
1782
|
-
case "analytics":
|
|
1783
|
-
await database.create({
|
|
1784
|
-
model: "bundle_events",
|
|
1785
|
-
data: change.row
|
|
1786
|
-
});
|
|
1787
|
-
return;
|
|
1788
1833
|
case "apiKeys": switch (change.operation) {
|
|
1789
1834
|
case "insert":
|
|
1790
1835
|
await database.create({
|
|
@@ -1856,10 +1901,10 @@ const hasOnlyKeys = (value, keys) => {
|
|
|
1856
1901
|
return Reflect.ownKeys(value).length === keys.length && keys.every((key) => Object.hasOwn(value, key));
|
|
1857
1902
|
};
|
|
1858
1903
|
const validateWhere = (where, field, validateValue = (value) => typeof value === "string") => {
|
|
1859
|
-
if (!isRecord$
|
|
1904
|
+
if (!isRecord$3(where) || !hasOnlyKeys(where, [field]) || !validateValue(Reflect.get(where, field))) throw new DatabasePluginInputError("invalid-data");
|
|
1860
1905
|
};
|
|
1861
1906
|
const validateDatabaseChange = (change) => {
|
|
1862
|
-
if (!isRecord$
|
|
1907
|
+
if (!isRecord$3(change)) throw new DatabasePluginInputError("invalid-data");
|
|
1863
1908
|
switch (change.model) {
|
|
1864
1909
|
case "bundles": switch (change.operation) {
|
|
1865
1910
|
case "insert":
|
|
@@ -1966,14 +2011,6 @@ const validateDatabaseChange = (change) => {
|
|
|
1966
2011
|
return;
|
|
1967
2012
|
default: throw new DatabasePluginInputError("invalid-operation");
|
|
1968
2013
|
}
|
|
1969
|
-
case "analytics":
|
|
1970
|
-
if (change.operation !== "insert" || !hasOnlyKeys(change, [
|
|
1971
|
-
"model",
|
|
1972
|
-
"operation",
|
|
1973
|
-
"row"
|
|
1974
|
-
])) throw new DatabasePluginInputError("invalid-operation");
|
|
1975
|
-
validateCreateData("bundle_events", change.row);
|
|
1976
|
-
return;
|
|
1977
2014
|
case "apiKeys": switch (change.operation) {
|
|
1978
2015
|
case "insert":
|
|
1979
2016
|
if (!hasOnlyKeys(change, [
|
|
@@ -1992,7 +2029,7 @@ const validateDatabaseChange = (change) => {
|
|
|
1992
2029
|
"update"
|
|
1993
2030
|
])) throw new DatabasePluginInputError("invalid-data");
|
|
1994
2031
|
validateWhere(change.where, "id");
|
|
1995
|
-
if (!isRecord$
|
|
2032
|
+
if (!isRecord$3(change.update)) throw new DatabasePluginInputError("invalid-data");
|
|
1996
2033
|
if (!hasOnlyKeys(change.update, ["revokedAtMs"])) throw new DatabasePluginInputError("invalid-data");
|
|
1997
2034
|
validateApiKeyUpdateData({ revoked_at_ms: change.update.revokedAtMs });
|
|
1998
2035
|
return;
|
|
@@ -2002,7 +2039,7 @@ const validateDatabaseChange = (change) => {
|
|
|
2002
2039
|
}
|
|
2003
2040
|
};
|
|
2004
2041
|
const validateDatabaseCommitExpectation = (expectation) => {
|
|
2005
|
-
if (!isRecord$
|
|
2042
|
+
if (!isRecord$3(expectation)) throw new DatabasePluginInputError("invalid-data");
|
|
2006
2043
|
if (expectation.model === "releases") {
|
|
2007
2044
|
if (!hasOnlyKeys(expectation, [
|
|
2008
2045
|
"model",
|
|
@@ -2022,7 +2059,7 @@ const validateDatabaseCommitExpectation = (expectation) => {
|
|
|
2022
2059
|
throw new DatabasePluginInputError("invalid-model");
|
|
2023
2060
|
};
|
|
2024
2061
|
function validateDatabaseCommit(input) {
|
|
2025
|
-
if (!isRecord$
|
|
2062
|
+
if (!isRecord$3(input) || !Array.isArray(input.changes) || (Object.hasOwn(input, "expectations") ? !hasOnlyKeys(input, ["changes", "expectations"]) || !Array.isArray(input.expectations) : !hasOnlyKeys(input, ["changes"]))) throw new DatabasePluginInputError("invalid-data");
|
|
2026
2063
|
input.changes.forEach(validateDatabaseChange);
|
|
2027
2064
|
if (Array.isArray(input.expectations)) input.expectations.forEach(validateDatabaseCommitExpectation);
|
|
2028
2065
|
}
|
|
@@ -2241,37 +2278,83 @@ const createDatabasePluginAdapter = (name, implementation) => {
|
|
|
2241
2278
|
return result;
|
|
2242
2279
|
}
|
|
2243
2280
|
},
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2281
|
+
insights: {
|
|
2282
|
+
record: (input) => implementation.recordInsights(input),
|
|
2283
|
+
async listEvents(input) {
|
|
2284
|
+
const ranges = await Promise.all(toInsightsEventRanges(input.filter).map((where) => listInsightsEventRange(crud, input, where)));
|
|
2285
|
+
if (ranges.length === 1) return ranges[0];
|
|
2286
|
+
return ranges.flat().sort((left, right) => right.received_at_ms - left.received_at_ms || compareInsightsText(right.id, left.id)).slice(0, input.limit);
|
|
2250
2287
|
},
|
|
2251
|
-
async
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
model: "bundle_events",
|
|
2288
|
+
async findInstallations(input) {
|
|
2289
|
+
if ("installId" in input) {
|
|
2290
|
+
const row = await crud.findOne({
|
|
2291
|
+
model: "bundle_installations",
|
|
2256
2292
|
where: [{
|
|
2257
|
-
field: "
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
}],
|
|
2261
|
-
orderBy: [{
|
|
2262
|
-
field: "received_at_ms",
|
|
2263
|
-
direction: "asc"
|
|
2264
|
-
}, {
|
|
2265
|
-
field: "id",
|
|
2266
|
-
direction: "asc"
|
|
2267
|
-
}],
|
|
2268
|
-
limit: PAGE_SIZE$1,
|
|
2269
|
-
offset
|
|
2293
|
+
field: "install_id",
|
|
2294
|
+
value: input.installId
|
|
2295
|
+
}]
|
|
2270
2296
|
});
|
|
2271
|
-
|
|
2272
|
-
if (page.length < PAGE_SIZE$1) break;
|
|
2297
|
+
return row === null ? [] : [row];
|
|
2273
2298
|
}
|
|
2274
|
-
return
|
|
2299
|
+
return crud.findMany({
|
|
2300
|
+
model: "bundle_installations",
|
|
2301
|
+
where: [{
|
|
2302
|
+
field: "user_id",
|
|
2303
|
+
value: input.userId
|
|
2304
|
+
}, ...input.afterInstallId === void 0 ? [] : [{
|
|
2305
|
+
field: "install_id",
|
|
2306
|
+
operator: "gt",
|
|
2307
|
+
value: input.afterInstallId
|
|
2308
|
+
}]],
|
|
2309
|
+
orderBy: [{
|
|
2310
|
+
field: "install_id",
|
|
2311
|
+
direction: "asc"
|
|
2312
|
+
}],
|
|
2313
|
+
limit: input.limit,
|
|
2314
|
+
offset: 0
|
|
2315
|
+
});
|
|
2316
|
+
},
|
|
2317
|
+
countInstallations(input) {
|
|
2318
|
+
return crud.count({
|
|
2319
|
+
model: "bundle_installations",
|
|
2320
|
+
where: [
|
|
2321
|
+
{
|
|
2322
|
+
field: "platform",
|
|
2323
|
+
value: input.platform
|
|
2324
|
+
},
|
|
2325
|
+
{
|
|
2326
|
+
field: "channel",
|
|
2327
|
+
value: input.channel
|
|
2328
|
+
},
|
|
2329
|
+
{
|
|
2330
|
+
field: "received_at_ms",
|
|
2331
|
+
operator: "gte",
|
|
2332
|
+
value: input.sinceMs
|
|
2333
|
+
},
|
|
2334
|
+
...input.bundleId === void 0 ? [] : [{
|
|
2335
|
+
field: "to_bundle_id",
|
|
2336
|
+
value: input.bundleId
|
|
2337
|
+
}]
|
|
2338
|
+
]
|
|
2339
|
+
});
|
|
2340
|
+
},
|
|
2341
|
+
countEvents(input) {
|
|
2342
|
+
return crud.count({
|
|
2343
|
+
model: "bundle_events",
|
|
2344
|
+
where: [
|
|
2345
|
+
...toInsightsBundleWhere(input.filter),
|
|
2346
|
+
{
|
|
2347
|
+
field: "received_at_ms",
|
|
2348
|
+
operator: "gte",
|
|
2349
|
+
value: input.sinceMs
|
|
2350
|
+
},
|
|
2351
|
+
{
|
|
2352
|
+
field: "received_at_ms",
|
|
2353
|
+
operator: "lt",
|
|
2354
|
+
value: input.beforeReceivedAtMs
|
|
2355
|
+
}
|
|
2356
|
+
]
|
|
2357
|
+
});
|
|
2275
2358
|
}
|
|
2276
2359
|
},
|
|
2277
2360
|
apiKeys: {
|
|
@@ -2315,7 +2398,13 @@ const createDatabasePluginAdapter = (name, implementation) => {
|
|
|
2315
2398
|
...implementation.dispose ? { dispose: implementation.dispose } : {}
|
|
2316
2399
|
};
|
|
2317
2400
|
};
|
|
2318
|
-
const createDatabasePlugin = (options) => ({
|
|
2401
|
+
const createDatabasePlugin = (options) => ({
|
|
2402
|
+
...options,
|
|
2403
|
+
models: {
|
|
2404
|
+
...options.models,
|
|
2405
|
+
insights: createValidatedInsightsModel(options.models.insights)
|
|
2406
|
+
}
|
|
2407
|
+
});
|
|
2319
2408
|
//#endregion
|
|
2320
2409
|
//#region ../plugin-core/dist/createStoragePlugin.mjs
|
|
2321
2410
|
const createStoragePlugin = (options) => ({ ...options });
|
|
@@ -2450,6 +2539,8 @@ const rowsToBundles = (bundleRows, patchRows, referencedBundleRows) => {
|
|
|
2450
2539
|
//#endregion
|
|
2451
2540
|
//#region ../plugin-core/dist/databaseClientUpdates.mjs
|
|
2452
2541
|
var DatabasePatchUpdateUnsupportedError = class extends Error {
|
|
2542
|
+
bundleId;
|
|
2543
|
+
pluginName;
|
|
2453
2544
|
name = "DatabasePatchUpdateUnsupportedError";
|
|
2454
2545
|
constructor(bundleId, pluginName) {
|
|
2455
2546
|
super(`Database plugin "${pluginName}" cannot atomically replace patches for bundle "${bundleId}".`);
|
|
@@ -2538,7 +2629,8 @@ const hydrateRows = async (database, ownerRows) => {
|
|
|
2538
2629
|
const patchRows = await database.models.bundlePatches.findByBundleIds(ownerRows.map(({ id }) => id));
|
|
2539
2630
|
const ownerIds = new Set(ownerRows.map(({ id }) => id));
|
|
2540
2631
|
const referencedIds = [...new Set(patchRows.map(({ base_bundle_id }) => base_bundle_id).filter((id) => !ownerIds.has(id)))];
|
|
2541
|
-
|
|
2632
|
+
const referencedRows = referencedIds.length === 0 ? [] : await loadBundleRows(database, { id: { in: referencedIds } });
|
|
2633
|
+
return rowsToBundles(ownerRows, patchRows, referencedRows);
|
|
2542
2634
|
};
|
|
2543
2635
|
const cursorIdFilter = (cursor, direction) => {
|
|
2544
2636
|
if (cursor?.after) return { [direction === "desc" ? "lt" : "gt"]: cursor.after };
|
|
@@ -2590,6 +2682,7 @@ const responsePage = async (database, options) => {
|
|
|
2590
2682
|
//#endregion
|
|
2591
2683
|
//#region ../plugin-core/dist/databaseClient.mjs
|
|
2592
2684
|
var DatabaseBundleNotFoundError = class extends Error {
|
|
2685
|
+
bundleId;
|
|
2593
2686
|
name = "DatabaseBundleNotFoundError";
|
|
2594
2687
|
constructor(bundleId) {
|
|
2595
2688
|
super(`Bundle "${bundleId}" was not found.`);
|
|
@@ -2597,6 +2690,8 @@ var DatabaseBundleNotFoundError = class extends Error {
|
|
|
2597
2690
|
}
|
|
2598
2691
|
};
|
|
2599
2692
|
var DatabasePatchInsertUnsupportedError = class extends Error {
|
|
2693
|
+
bundleId;
|
|
2694
|
+
pluginName;
|
|
2600
2695
|
name = "DatabasePatchInsertUnsupportedError";
|
|
2601
2696
|
constructor(bundleId, pluginName) {
|
|
2602
2697
|
super(`Database plugin "${pluginName}" cannot atomically insert patches for bundle "${bundleId}".`);
|
|
@@ -2615,12 +2710,13 @@ const insertChanges = (bundle) => [{
|
|
|
2615
2710
|
}))];
|
|
2616
2711
|
const updateChanges = (bundleId, update) => {
|
|
2617
2712
|
const rowUpdate = bundleUpdateToRow(update);
|
|
2713
|
+
const patchesPresent = Object.hasOwn(update, "patches");
|
|
2618
2714
|
return [{
|
|
2619
2715
|
model: "bundles",
|
|
2620
2716
|
operation: "update",
|
|
2621
2717
|
where: { id: bundleId },
|
|
2622
2718
|
update: rowUpdate
|
|
2623
|
-
}, ...
|
|
2719
|
+
}, ...patchesPresent ? [{
|
|
2624
2720
|
model: "bundlePatches",
|
|
2625
2721
|
operation: "delete",
|
|
2626
2722
|
where: { bundleId }
|
|
@@ -2693,7 +2789,7 @@ const createDatabaseClient = (plugin) => {
|
|
|
2693
2789
|
};
|
|
2694
2790
|
};
|
|
2695
2791
|
//#endregion
|
|
2696
|
-
//#region ../plugin-core/dist/node_modules/.pnpm/verkit@0.
|
|
2792
|
+
//#region ../plugin-core/dist/node_modules/.pnpm/verkit@0.4.0/node_modules/verkit/dist/comparison-DenM3wCn.mjs
|
|
2697
2793
|
const LETTER_DASH_NUMBER = "[a-zA-Z0-9-]";
|
|
2698
2794
|
const NUMERIC_IDENTIFIER = String.raw`0|[1-9]\d*`;
|
|
2699
2795
|
const NUMERIC_IDENTIFIER_LOOSE = String.raw`\d+`;
|
|
@@ -2730,22 +2826,9 @@ function makeSafeRegexSource(source) {
|
|
|
2730
2826
|
function safeRegex(source, flags) {
|
|
2731
2827
|
return new RegExp(makeSafeRegexSource(source), flags);
|
|
2732
2828
|
}
|
|
2733
|
-
const NUMERIC$1 = /^\d+$/;
|
|
2734
|
-
function compareIdentifiers(left, right) {
|
|
2735
|
-
if (typeof left === "number" && typeof right === "number") return left === right ? 0 : left < right ? -1 : 1;
|
|
2736
|
-
const leftNumeric = NUMERIC$1.test(String(left));
|
|
2737
|
-
const rightNumeric = NUMERIC$1.test(String(right));
|
|
2738
|
-
const normalizedLeft = leftNumeric ? Number(left) : left;
|
|
2739
|
-
const normalizedRight = rightNumeric ? Number(right) : right;
|
|
2740
|
-
return normalizedLeft === normalizedRight ? 0 : leftNumeric && !rightNumeric ? -1 : rightNumeric && !leftNumeric ? 1 : normalizedLeft < normalizedRight ? -1 : 1;
|
|
2741
|
-
}
|
|
2742
2829
|
const FULL = safeRegex(`^${FULL_PLAIN}$`);
|
|
2743
2830
|
const LOOSE = safeRegex(`^${LOOSE_PLAIN}$`);
|
|
2744
|
-
|
|
2745
|
-
safeRegex(`^${PRERELEASE_LOOSE}$`);
|
|
2746
|
-
const COERCE_EXACT = safeRegex(COERCE);
|
|
2747
|
-
const COERCE_FULL_EXACT = safeRegex(COERCE_FULL);
|
|
2748
|
-
const NUMERIC = /^\d+$/;
|
|
2831
|
+
const NUMERIC$1 = /^\d+$/;
|
|
2749
2832
|
function formatComparableVersion(version) {
|
|
2750
2833
|
const base = `${version.major}.${version.minor}.${version.patch}`;
|
|
2751
2834
|
return version.prerelease?.length ? `${base}-${version.prerelease.join(".")}` : base;
|
|
@@ -2766,7 +2849,7 @@ function parse(version, options = {}) {
|
|
|
2766
2849
|
if (minor > Number.MAX_SAFE_INTEGER || minor < 0) throw new TypeError(`Invalid minor version: ${match[2]}`);
|
|
2767
2850
|
if (patch > Number.MAX_SAFE_INTEGER || patch < 0) throw new TypeError(`Invalid patch version: ${match[3]}`);
|
|
2768
2851
|
const prerelease = match[4] ? match[4].split(".").map((identifier) => {
|
|
2769
|
-
if (NUMERIC.test(identifier)) {
|
|
2852
|
+
if (NUMERIC$1.test(identifier)) {
|
|
2770
2853
|
const numeric = Number(identifier);
|
|
2771
2854
|
if (numeric >= 0 && numeric < Number.MAX_SAFE_INTEGER) return numeric;
|
|
2772
2855
|
}
|
|
@@ -2787,6 +2870,15 @@ function tryParse(version, options = {}) {
|
|
|
2787
2870
|
return null;
|
|
2788
2871
|
}
|
|
2789
2872
|
}
|
|
2873
|
+
const NUMERIC = /^\d+$/;
|
|
2874
|
+
function compareIdentifiers(left, right) {
|
|
2875
|
+
if (typeof left === "number" && typeof right === "number") return left === right ? 0 : left < right ? -1 : 1;
|
|
2876
|
+
const leftNumeric = NUMERIC.test(String(left));
|
|
2877
|
+
const rightNumeric = NUMERIC.test(String(right));
|
|
2878
|
+
const normalizedLeft = leftNumeric ? Number(left) : left;
|
|
2879
|
+
const normalizedRight = rightNumeric ? Number(right) : right;
|
|
2880
|
+
return normalizedLeft === normalizedRight ? 0 : leftNumeric && !rightNumeric ? -1 : rightNumeric && !leftNumeric ? 1 : normalizedLeft < normalizedRight ? -1 : 1;
|
|
2881
|
+
}
|
|
2790
2882
|
function compareMainParsed(left, right) {
|
|
2791
2883
|
return left.major === right.major ? left.minor === right.minor ? left.patch === right.patch ? 0 : left.patch < right.patch ? -1 : 1 : left.minor < right.minor ? -1 : 1 : left.major < right.major ? -1 : 1;
|
|
2792
2884
|
}
|
|
@@ -2808,26 +2900,15 @@ function comparePrereleaseParsed(left, right) {
|
|
|
2808
2900
|
function compareParsed(left, right) {
|
|
2809
2901
|
return compareMainParsed(left, right) || comparePrereleaseParsed(left, right);
|
|
2810
2902
|
}
|
|
2811
|
-
function
|
|
2812
|
-
|
|
2813
|
-
const input = typeof value === "number" ? String(value) : value;
|
|
2814
|
-
if (typeof input !== "string") return null;
|
|
2815
|
-
let match = null;
|
|
2816
|
-
if (options.rtl) {
|
|
2817
|
-
const expression = safeRegex(options.includePrerelease ? COERCE_FULL : COERCE, "g");
|
|
2818
|
-
let next;
|
|
2819
|
-
while ((next = expression.exec(input)) && (!match || match.index + match[0].length !== input.length)) {
|
|
2820
|
-
if (!match || next.index + next[0].length !== match.index + match[0].length) match = next;
|
|
2821
|
-
expression.lastIndex = next.index + next[1].length + next[2].length;
|
|
2822
|
-
}
|
|
2823
|
-
} else match = (options.includePrerelease ? COERCE_FULL_EXACT : COERCE_EXACT).exec(input);
|
|
2824
|
-
if (!match) return null;
|
|
2825
|
-
const major = match[2];
|
|
2826
|
-
return tryParse(`${major}.${match[3] || "0"}.${match[4] || "0"}${options.includePrerelease && match[5] ? `-${match[5]}` : ""}${options.includePrerelease && match[6] ? `+${match[6]}` : ""}`, options);
|
|
2903
|
+
function compare$1(left, right, options = {}) {
|
|
2904
|
+
return compareParsed(parse(left, options), parse(right, options));
|
|
2827
2905
|
}
|
|
2906
|
+
//#endregion
|
|
2907
|
+
//#region ../plugin-core/dist/node_modules/.pnpm/verkit@0.4.0/node_modules/verkit/dist/set-CC5YeoYX.mjs
|
|
2828
2908
|
const STRICT_COMPARATOR = safeRegex(String.raw`^${GREATER_LESS_THAN}\s*(${FULL_PLAIN})$|^$`);
|
|
2829
2909
|
const LOOSE_COMPARATOR$1 = safeRegex(String.raw`^${GREATER_LESS_THAN}\s*(${LOOSE_PLAIN})$|^$`);
|
|
2830
2910
|
function parseComparator(comparator, options = {}) {
|
|
2911
|
+
if (typeof comparator !== "string") return comparator;
|
|
2831
2912
|
const normalized = comparator.trim().replaceAll(/\s+/g, " ");
|
|
2832
2913
|
const match = normalized.match(options.loose ? LOOSE_COMPARATOR$1 : STRICT_COMPARATOR);
|
|
2833
2914
|
if (!match) throw new TypeError(`Invalid comparator: ${normalized}`);
|
|
@@ -2840,9 +2921,8 @@ function parseComparator(comparator, options = {}) {
|
|
|
2840
2921
|
version
|
|
2841
2922
|
};
|
|
2842
2923
|
}
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
}
|
|
2924
|
+
//#endregion
|
|
2925
|
+
//#region ../plugin-core/dist/node_modules/.pnpm/verkit@0.4.0/node_modules/verkit/dist/range-DvX-Y6iv.mjs
|
|
2846
2926
|
const BUILD_STRIP = new RegExp(BUILD, "g");
|
|
2847
2927
|
const BUILD_SAFE = safeRegex(BUILD);
|
|
2848
2928
|
const STRICT_HYPHEN = safeRegex(String.raw`^\s*(${XRANGE_PLAIN})\s+-\s+(${XRANGE_PLAIN})\s*$`);
|
|
@@ -2968,9 +3048,9 @@ function parseSimpleRange(input, options) {
|
|
|
2968
3048
|
function parseRange(range, options = {}) {
|
|
2969
3049
|
if (typeof range !== "string") return range;
|
|
2970
3050
|
const parsedOptions = { ...options };
|
|
2971
|
-
const
|
|
2972
|
-
let sets =
|
|
2973
|
-
if (!sets.length) throw new TypeError(`Range contains no valid comparator sets: ${
|
|
3051
|
+
const normalizedRange = range.trim().replaceAll(/\s+/g, " ");
|
|
3052
|
+
let sets = normalizedRange.split("||").map((part) => parseSimpleRange(part.trim(), parsedOptions)).filter((set) => set.length);
|
|
3053
|
+
if (!sets.length) throw new TypeError(`Range contains no valid comparator sets: ${normalizedRange}`);
|
|
2974
3054
|
if (sets.length > 1) {
|
|
2975
3055
|
const first = sets[0];
|
|
2976
3056
|
sets = sets.filter((set) => set[0]?.value !== "<0.0.0-0");
|
|
@@ -2981,19 +3061,39 @@ function parseRange(range, options = {}) {
|
|
|
2981
3061
|
}
|
|
2982
3062
|
}
|
|
2983
3063
|
return {
|
|
2984
|
-
normalized: sets.map((set) => set.map((comparator) => comparator.value).join(" ")).join("||"),
|
|
2985
3064
|
options: parsedOptions,
|
|
2986
|
-
raw,
|
|
2987
3065
|
sets
|
|
2988
3066
|
};
|
|
2989
3067
|
}
|
|
3068
|
+
//#endregion
|
|
3069
|
+
//#region ../plugin-core/dist/node_modules/.pnpm/verkit@0.4.0/node_modules/verkit/dist/version-CQ98ZBpL.mjs
|
|
3070
|
+
const COERCE_EXACT = safeRegex(COERCE);
|
|
3071
|
+
const COERCE_FULL_EXACT = safeRegex(COERCE_FULL);
|
|
3072
|
+
safeRegex(`^${PRERELEASE}$`);
|
|
3073
|
+
safeRegex(`^${PRERELEASE_LOOSE}$`);
|
|
3074
|
+
function normalizeFull(version, options = {}) {
|
|
3075
|
+
const parsed = tryParse(version, options);
|
|
3076
|
+
return parsed ? formatFullVersion(parsed) : null;
|
|
3077
|
+
}
|
|
2990
3078
|
function normalize(version, options = {}) {
|
|
2991
3079
|
const parsed = tryParse(version, options);
|
|
2992
3080
|
return parsed ? formatComparableVersion(parsed) : null;
|
|
2993
3081
|
}
|
|
2994
3082
|
function coerce(value, options = {}) {
|
|
2995
|
-
|
|
2996
|
-
|
|
3083
|
+
if (typeof value === "object") return value;
|
|
3084
|
+
const input = typeof value === "number" ? String(value) : value;
|
|
3085
|
+
let match = null;
|
|
3086
|
+
if (options.rtl) {
|
|
3087
|
+
const expression = safeRegex(options.includePrerelease ? COERCE_FULL : COERCE, "g");
|
|
3088
|
+
let next;
|
|
3089
|
+
while ((next = expression.exec(input)) && (!match || match.index + match[0].length !== input.length)) {
|
|
3090
|
+
if (!match || next.index + next[0].length !== match.index + match[0].length) match = next;
|
|
3091
|
+
expression.lastIndex = next.index + next[1].length + next[2].length;
|
|
3092
|
+
}
|
|
3093
|
+
} else match = (options.includePrerelease ? COERCE_FULL_EXACT : COERCE_EXACT).exec(input);
|
|
3094
|
+
if (!match) return null;
|
|
3095
|
+
const major = match[2];
|
|
3096
|
+
return tryParse(`${major}.${match[3] || "0"}.${match[4] || "0"}${options.includePrerelease && match[5] ? `-${match[5]}` : ""}${options.includePrerelease && match[6] ? `+${match[6]}` : ""}`, options);
|
|
2997
3097
|
}
|
|
2998
3098
|
//#endregion
|
|
2999
3099
|
//#region ../plugin-core/dist/releaseCatalogCompiler.mjs
|
|
@@ -3273,15 +3373,16 @@ function compileAppVersion(releases) {
|
|
|
3273
3373
|
const retainedIds = new Set(segmentReleases.flatMap(({ retainedIds, rollbackIds }) => [...retainedIds, ...rollbackIds]));
|
|
3274
3374
|
const retainedReleases = releases.filter((release) => retainedIds.has(release.id));
|
|
3275
3375
|
const descriptorIndex = new Map(retainedReleases.map((release, index) => [release.id, index]));
|
|
3376
|
+
const segments = mergeSegments(segmentReleases.map(({ segment, retainedIds, rollbackIds }) => ({
|
|
3377
|
+
...segment,
|
|
3378
|
+
releaseIndexes: releases.filter((release) => retainedIds.has(release.id)).map((release) => descriptorIndex.get(release.id)).filter((index) => index !== void 0),
|
|
3379
|
+
rollbackReleaseIndexes: releases.filter((release) => rollbackIds.has(release.id)).map((release) => descriptorIndex.get(release.id)).filter((index) => index !== void 0)
|
|
3380
|
+
})).filter((segment) => segment.releaseIndexes.length > 0 || segment.rollbackReleaseIndexes.length > 0));
|
|
3276
3381
|
return {
|
|
3277
3382
|
envelope: {
|
|
3278
3383
|
fallbackPolicy: RELEASE_CATALOG_FALLBACK_POLICY,
|
|
3279
3384
|
schemaVersion: 1,
|
|
3280
|
-
segments
|
|
3281
|
-
...segment,
|
|
3282
|
-
releaseIndexes: releases.filter((release) => retainedIds.has(release.id)).map((release) => descriptorIndex.get(release.id)).filter((index) => index !== void 0),
|
|
3283
|
-
rollbackReleaseIndexes: releases.filter((release) => rollbackIds.has(release.id)).map((release) => descriptorIndex.get(release.id)).filter((index) => index !== void 0)
|
|
3284
|
-
})).filter((segment) => segment.releaseIndexes.length > 0 || segment.rollbackReleaseIndexes.length > 0)),
|
|
3385
|
+
segments,
|
|
3285
3386
|
strategy: "APP_VERSION"
|
|
3286
3387
|
},
|
|
3287
3388
|
retainedReleases
|
|
@@ -3349,7 +3450,8 @@ function versionInSegment(version, segment) {
|
|
|
3349
3450
|
return true;
|
|
3350
3451
|
}
|
|
3351
3452
|
function canonicalizeAppVersion(appVersion) {
|
|
3352
|
-
|
|
3453
|
+
const version = coerce(appVersion);
|
|
3454
|
+
return version ? normalizeFull(version) : null;
|
|
3353
3455
|
}
|
|
3354
3456
|
function projectCompiledCatalog(catalog, appVersion) {
|
|
3355
3457
|
let indexes;
|
|
@@ -3377,6 +3479,7 @@ function projectCompiledRollbackCatalog(catalog, appVersion) {
|
|
|
3377
3479
|
const RELEASE_PAGE_SIZE = 1e3;
|
|
3378
3480
|
const DEFAULT_MAX_ATTEMPTS = 3;
|
|
3379
3481
|
var ReleaseCatalogMutationError = class extends Error {
|
|
3482
|
+
code;
|
|
3380
3483
|
name = "ReleaseCatalogMutationError";
|
|
3381
3484
|
constructor(code, message) {
|
|
3382
3485
|
super(message);
|
|
@@ -3636,6 +3739,7 @@ async function rebuildReleaseCatalog(input) {
|
|
|
3636
3739
|
//#endregion
|
|
3637
3740
|
//#region ../plugin-core/dist/releaseManagement.mjs
|
|
3638
3741
|
var ReleaseManagementError = class extends Error {
|
|
3742
|
+
code;
|
|
3639
3743
|
name = "ReleaseManagementError";
|
|
3640
3744
|
constructor(code, message) {
|
|
3641
3745
|
super(message);
|
|
@@ -3715,7 +3819,7 @@ async function deleteRelease(input) {
|
|
|
3715
3819
|
}
|
|
3716
3820
|
//#endregion
|
|
3717
3821
|
//#region ../plugin-core/dist/storageDownloadPath.mjs
|
|
3718
|
-
const decodeBase64Url = (value) => {
|
|
3822
|
+
const decodeBase64Url$1 = (value) => {
|
|
3719
3823
|
try {
|
|
3720
3824
|
const base64 = value.replaceAll("-", "+").replaceAll("_", "/");
|
|
3721
3825
|
const binary = atob(base64.padEnd(Math.ceil(base64.length / 4) * 4, "="));
|
|
@@ -3727,7 +3831,7 @@ const decodeBase64Url = (value) => {
|
|
|
3727
3831
|
const parseStorageDownloadPath = (path) => {
|
|
3728
3832
|
const match = /^\/storage\/([^/]+)\/([^/]+)$/.exec(path);
|
|
3729
3833
|
if (!match) return null;
|
|
3730
|
-
const storageUri = decodeBase64Url(match[1]);
|
|
3834
|
+
const storageUri = decodeBase64Url$1(match[1]);
|
|
3731
3835
|
if (storageUri === null) return null;
|
|
3732
3836
|
try {
|
|
3733
3837
|
return {
|
|
@@ -3848,7 +3952,7 @@ const createReleaseCatalogRouteHandlers = (clientAccessHeaderName = "x-api-key")
|
|
|
3848
3952
|
//#endregion
|
|
3849
3953
|
//#region ../../packages/server/dist/handlerReleaseManagementRoutes.mjs
|
|
3850
3954
|
const unavailable = () => Response.json({ error: "Not found" }, { status: 404 });
|
|
3851
|
-
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3955
|
+
const isRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3852
3956
|
const parseRevision = (value) => {
|
|
3853
3957
|
if (value === void 0 || value === null || value === "") return void 0;
|
|
3854
3958
|
const revision = typeof value === "number" ? value : Number(value);
|
|
@@ -3857,7 +3961,7 @@ const parseRevision = (value) => {
|
|
|
3857
3961
|
};
|
|
3858
3962
|
const parsePolicyInput = async (request) => {
|
|
3859
3963
|
const body = await request.json();
|
|
3860
|
-
if (!isRecord(body) || !isRecord(body.patch)) throw new HandlerBadRequestError("Invalid Release policy mutation");
|
|
3964
|
+
if (!isRecord$2(body) || !isRecord$2(body.patch)) throw new HandlerBadRequestError("Invalid Release policy mutation");
|
|
3861
3965
|
return {
|
|
3862
3966
|
expectedRevision: parseRevision(body.expectedRevision),
|
|
3863
3967
|
patch: body.patch
|
|
@@ -3966,6 +4070,310 @@ const createReleaseManagementRouteHandlers = () => ({
|
|
|
3966
4070
|
}
|
|
3967
4071
|
});
|
|
3968
4072
|
//#endregion
|
|
4073
|
+
//#region ../../packages/server/dist/insights/errors.mjs
|
|
4074
|
+
var InsightsBadRequestError = class extends Error {
|
|
4075
|
+
name = "InsightsBadRequestError";
|
|
4076
|
+
};
|
|
4077
|
+
var InsightsPayloadTooLargeError = class extends Error {
|
|
4078
|
+
maximumBytes;
|
|
4079
|
+
name = "InsightsPayloadTooLargeError";
|
|
4080
|
+
constructor(maximumBytes) {
|
|
4081
|
+
super(`Event payload exceeds ${maximumBytes} bytes`);
|
|
4082
|
+
this.maximumBytes = maximumBytes;
|
|
4083
|
+
}
|
|
4084
|
+
};
|
|
4085
|
+
//#endregion
|
|
4086
|
+
//#region ../../packages/server/dist/insights/eventInput.mjs
|
|
4087
|
+
const MAX_EVENT_STRING_LENGTH = 1024;
|
|
4088
|
+
const MAX_IDENTITY_LENGTH$2 = 255;
|
|
4089
|
+
const EVENT_BODY_MAX_BYTES = 16384;
|
|
4090
|
+
const eventKeys = /* @__PURE__ */ new Set([
|
|
4091
|
+
"type",
|
|
4092
|
+
"installId",
|
|
4093
|
+
"toBundleId",
|
|
4094
|
+
"userId",
|
|
4095
|
+
"username",
|
|
4096
|
+
"platform",
|
|
4097
|
+
"appVersion",
|
|
4098
|
+
"channel",
|
|
4099
|
+
"cohort",
|
|
4100
|
+
"fingerprintHash",
|
|
4101
|
+
"fromBundleId",
|
|
4102
|
+
"fromReleaseId",
|
|
4103
|
+
"toReleaseId",
|
|
4104
|
+
"updateStrategy",
|
|
4105
|
+
"sdkVersion"
|
|
4106
|
+
]);
|
|
4107
|
+
function isRecord$1(value) {
|
|
4108
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4109
|
+
}
|
|
4110
|
+
function requireStringField(payload, key) {
|
|
4111
|
+
const value = payload[key];
|
|
4112
|
+
if (typeof value !== "string" || value.length === 0 || value.length > MAX_EVENT_STRING_LENGTH || new TextDecoder("utf-8", { ignoreBOM: true }).decode(new TextEncoder().encode(value)) !== value) throw new InsightsBadRequestError(`Invalid event field: ${key}`);
|
|
4113
|
+
return value;
|
|
4114
|
+
}
|
|
4115
|
+
function requireNullableStringField(payload, key) {
|
|
4116
|
+
if (payload[key] === null) return null;
|
|
4117
|
+
return requireStringField(payload, key);
|
|
4118
|
+
}
|
|
4119
|
+
function requireIdentityField(payload, key) {
|
|
4120
|
+
const value = requireStringField(payload, key);
|
|
4121
|
+
if (value.length > MAX_IDENTITY_LENGTH$2) throw new InsightsBadRequestError(`Invalid event field: ${key}`);
|
|
4122
|
+
return value;
|
|
4123
|
+
}
|
|
4124
|
+
async function readBoundedText(request) {
|
|
4125
|
+
const contentLength = request.headers.get("content-length");
|
|
4126
|
+
const declaredByteLength = Number(contentLength);
|
|
4127
|
+
if (contentLength !== null && Number.isSafeInteger(declaredByteLength) && declaredByteLength > 16384) throw new InsightsPayloadTooLargeError(EVENT_BODY_MAX_BYTES);
|
|
4128
|
+
if (request.body === null) return "";
|
|
4129
|
+
const reader = request.body.getReader();
|
|
4130
|
+
const decoder = new TextDecoder();
|
|
4131
|
+
let byteLength = 0;
|
|
4132
|
+
let text = "";
|
|
4133
|
+
while (true) {
|
|
4134
|
+
const result = await reader.read();
|
|
4135
|
+
if (result.done) break;
|
|
4136
|
+
byteLength += result.value.byteLength;
|
|
4137
|
+
if (byteLength > 16384) {
|
|
4138
|
+
await reader.cancel();
|
|
4139
|
+
throw new InsightsPayloadTooLargeError(EVENT_BODY_MAX_BYTES);
|
|
4140
|
+
}
|
|
4141
|
+
text += decoder.decode(result.value, { stream: true });
|
|
4142
|
+
}
|
|
4143
|
+
return text + decoder.decode();
|
|
4144
|
+
}
|
|
4145
|
+
async function parseJson(request) {
|
|
4146
|
+
const text = await readBoundedText(request);
|
|
4147
|
+
try {
|
|
4148
|
+
return JSON.parse(text);
|
|
4149
|
+
} catch (error) {
|
|
4150
|
+
if (error instanceof SyntaxError) throw new InsightsBadRequestError("Invalid event payload");
|
|
4151
|
+
throw error;
|
|
4152
|
+
}
|
|
4153
|
+
}
|
|
4154
|
+
function requireEvent(payload) {
|
|
4155
|
+
if (!isRecord$1(payload) || Object.keys(payload).some((key) => !eventKeys.has(key))) throw new InsightsBadRequestError("Invalid event payload");
|
|
4156
|
+
const platform = requireStringField(payload, "platform");
|
|
4157
|
+
if (platform !== "ios" && platform !== "android") throw new InsightsBadRequestError("Invalid event field: platform");
|
|
4158
|
+
const base = {
|
|
4159
|
+
installId: requireIdentityField(payload, "installId"),
|
|
4160
|
+
toBundleId: requireStringField(payload, "toBundleId"),
|
|
4161
|
+
...payload.userId === void 0 ? {} : { userId: requireIdentityField(payload, "userId") },
|
|
4162
|
+
...payload.username === void 0 ? {} : { username: requireStringField(payload, "username") },
|
|
4163
|
+
platform,
|
|
4164
|
+
appVersion: requireStringField(payload, "appVersion"),
|
|
4165
|
+
channel: requireStringField(payload, "channel"),
|
|
4166
|
+
cohort: requireStringField(payload, "cohort"),
|
|
4167
|
+
fingerprintHash: requireNullableStringField(payload, "fingerprintHash"),
|
|
4168
|
+
sdkVersion: payload.sdkVersion === void 0 ? null : requireNullableStringField(payload, "sdkVersion"),
|
|
4169
|
+
fromReleaseId: requireNullableStringField(payload, "fromReleaseId"),
|
|
4170
|
+
toReleaseId: requireNullableStringField(payload, "toReleaseId")
|
|
4171
|
+
};
|
|
4172
|
+
const type = requireStringField(payload, "type");
|
|
4173
|
+
switch (type) {
|
|
4174
|
+
case "UPDATE_APPLIED":
|
|
4175
|
+
case "RECOVERED":
|
|
4176
|
+
case "RELEASE_ADOPTED": {
|
|
4177
|
+
const updateStrategy = requireStringField(payload, "updateStrategy");
|
|
4178
|
+
if (updateStrategy !== "fingerprint" && updateStrategy !== "appVersion") throw new InsightsBadRequestError("Invalid event field: updateStrategy");
|
|
4179
|
+
return {
|
|
4180
|
+
...base,
|
|
4181
|
+
type,
|
|
4182
|
+
fromBundleId: requireStringField(payload, "fromBundleId"),
|
|
4183
|
+
updateStrategy
|
|
4184
|
+
};
|
|
4185
|
+
}
|
|
4186
|
+
case "UNCHANGED":
|
|
4187
|
+
if (payload.fromBundleId !== null || payload.updateStrategy !== null) throw new InsightsBadRequestError("Invalid unchanged event shape");
|
|
4188
|
+
return {
|
|
4189
|
+
...base,
|
|
4190
|
+
type,
|
|
4191
|
+
fromBundleId: null,
|
|
4192
|
+
updateStrategy: null
|
|
4193
|
+
};
|
|
4194
|
+
default: throw new InsightsBadRequestError("Invalid event field: type");
|
|
4195
|
+
}
|
|
4196
|
+
}
|
|
4197
|
+
async function parseBundleEventRequest(request) {
|
|
4198
|
+
return requireEvent(await parseJson(request));
|
|
4199
|
+
}
|
|
4200
|
+
function createBundleEventRow(input) {
|
|
4201
|
+
input = requireEvent(input);
|
|
4202
|
+
const base = {
|
|
4203
|
+
app_version: input.appVersion,
|
|
4204
|
+
channel: input.channel,
|
|
4205
|
+
cohort: input.cohort,
|
|
4206
|
+
fingerprint_hash: input.fingerprintHash,
|
|
4207
|
+
from_release_id: input.fromReleaseId,
|
|
4208
|
+
id: createUUIDv7(),
|
|
4209
|
+
install_id: input.installId,
|
|
4210
|
+
platform: input.platform,
|
|
4211
|
+
received_at_ms: Date.now(),
|
|
4212
|
+
sdk_version: input.sdkVersion ?? null,
|
|
4213
|
+
to_bundle_id: input.toBundleId,
|
|
4214
|
+
to_release_id: input.toReleaseId,
|
|
4215
|
+
user_id: input.userId ?? null,
|
|
4216
|
+
username: input.username ?? null
|
|
4217
|
+
};
|
|
4218
|
+
switch (input.type) {
|
|
4219
|
+
case "UPDATE_APPLIED":
|
|
4220
|
+
case "RECOVERED":
|
|
4221
|
+
case "RELEASE_ADOPTED": return {
|
|
4222
|
+
...base,
|
|
4223
|
+
from_bundle_id: input.fromBundleId,
|
|
4224
|
+
type: input.type,
|
|
4225
|
+
update_strategy: input.updateStrategy
|
|
4226
|
+
};
|
|
4227
|
+
case "UNCHANGED": return {
|
|
4228
|
+
...base,
|
|
4229
|
+
from_bundle_id: null,
|
|
4230
|
+
type: input.type,
|
|
4231
|
+
update_strategy: null
|
|
4232
|
+
};
|
|
4233
|
+
}
|
|
4234
|
+
}
|
|
4235
|
+
//#endregion
|
|
4236
|
+
//#region ../../packages/server/dist/insights/queryInput.mjs
|
|
4237
|
+
const MAX_PAGE_LIMIT$1 = 100;
|
|
4238
|
+
const MAX_IDENTITY_LENGTH$1 = 255;
|
|
4239
|
+
const MAX_CURSOR_LENGTH$1 = 8192;
|
|
4240
|
+
const readSingle = (url, key) => {
|
|
4241
|
+
const values = url.searchParams.getAll(key);
|
|
4242
|
+
if (values.length > 1) throw new InsightsBadRequestError(`Duplicate '${key}' query parameter.`);
|
|
4243
|
+
return values[0];
|
|
4244
|
+
};
|
|
4245
|
+
const readPageLimit = (url) => {
|
|
4246
|
+
const value = readSingle(url, "limit");
|
|
4247
|
+
if (value === void 0) return void 0;
|
|
4248
|
+
const limit = Number(value);
|
|
4249
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_PAGE_LIMIT$1) throw new InsightsBadRequestError("Invalid 'limit' query parameter.");
|
|
4250
|
+
return limit;
|
|
4251
|
+
};
|
|
4252
|
+
const readCursor = (url) => {
|
|
4253
|
+
const cursor = readSingle(url, "cursor");
|
|
4254
|
+
if (cursor !== void 0 && (cursor.length === 0 || cursor.length > MAX_CURSOR_LENGTH$1)) throw new InsightsBadRequestError("Invalid 'cursor' query parameter.");
|
|
4255
|
+
return cursor;
|
|
4256
|
+
};
|
|
4257
|
+
const readId = (url, key, maximumLength = MAX_IDENTITY_LENGTH$1) => {
|
|
4258
|
+
const value = readSingle(url, key);
|
|
4259
|
+
if (value === void 0 || value.length === 0 || value.length > maximumLength) throw new InsightsBadRequestError(`Invalid '${key}' query parameter.`);
|
|
4260
|
+
return value;
|
|
4261
|
+
};
|
|
4262
|
+
const readTimestamp = (url, key) => {
|
|
4263
|
+
const raw = readSingle(url, key);
|
|
4264
|
+
if (raw === void 0) return void 0;
|
|
4265
|
+
const value = Number(raw);
|
|
4266
|
+
if (!raw.length || !Number.isSafeInteger(value) || value < 0) throw new InsightsBadRequestError(`Invalid '${key}' query parameter.`);
|
|
4267
|
+
return value;
|
|
4268
|
+
};
|
|
4269
|
+
const readScope$1 = (url) => {
|
|
4270
|
+
const platform = readSingle(url, "platform");
|
|
4271
|
+
if (platform !== "ios" && platform !== "android") throw new InsightsBadRequestError("Invalid 'platform' query parameter.");
|
|
4272
|
+
return {
|
|
4273
|
+
platform,
|
|
4274
|
+
channel: readId(url, "channel", 1024)
|
|
4275
|
+
};
|
|
4276
|
+
};
|
|
4277
|
+
const parseEventPageInput = (request) => {
|
|
4278
|
+
const url = new URL(request.url);
|
|
4279
|
+
const bundleFields = [
|
|
4280
|
+
"bundleId",
|
|
4281
|
+
"outcome",
|
|
4282
|
+
"platform",
|
|
4283
|
+
"channel"
|
|
4284
|
+
];
|
|
4285
|
+
let bundle;
|
|
4286
|
+
if (bundleFields.some((key) => url.searchParams.has(key))) {
|
|
4287
|
+
const outcome = readSingle(url, "outcome");
|
|
4288
|
+
if (outcome !== "applied" && outcome !== "recovered" && outcome !== "adopted") throw new InsightsBadRequestError("Invalid 'outcome' query parameter.");
|
|
4289
|
+
bundle = {
|
|
4290
|
+
...readScope$1(url),
|
|
4291
|
+
bundleId: readId(url, "bundleId", 1024),
|
|
4292
|
+
outcome
|
|
4293
|
+
};
|
|
4294
|
+
}
|
|
4295
|
+
return {
|
|
4296
|
+
beforeReceivedAtMs: readTimestamp(url, "beforeReceivedAtMs"),
|
|
4297
|
+
sinceMs: readTimestamp(url, "sinceMs"),
|
|
4298
|
+
cursor: readCursor(url),
|
|
4299
|
+
limit: readPageLimit(url),
|
|
4300
|
+
...bundle === void 0 ? {} : { bundle }
|
|
4301
|
+
};
|
|
4302
|
+
};
|
|
4303
|
+
const parseUserInstallationPageInput = (request) => {
|
|
4304
|
+
const url = new URL(request.url);
|
|
4305
|
+
const cursor = readCursor(url);
|
|
4306
|
+
const limit = readPageLimit(url);
|
|
4307
|
+
return {
|
|
4308
|
+
userId: readId(url, "userId"),
|
|
4309
|
+
...cursor === void 0 ? {} : { cursor },
|
|
4310
|
+
...limit === void 0 ? {} : { limit }
|
|
4311
|
+
};
|
|
4312
|
+
};
|
|
4313
|
+
const parseReportingOverviewInput = (request) => {
|
|
4314
|
+
const url = new URL(request.url);
|
|
4315
|
+
const window = readSingle(url, "window") ?? "30d";
|
|
4316
|
+
if (window !== "24h" && window !== "7d" && window !== "30d") throw new InsightsBadRequestError("Invalid 'window' query parameter.");
|
|
4317
|
+
const bundleId = readSingle(url, "bundleId");
|
|
4318
|
+
return {
|
|
4319
|
+
...readScope$1(url),
|
|
4320
|
+
window,
|
|
4321
|
+
...bundleId === void 0 ? {} : { bundleId: readId(url, "bundleId", 1024) }
|
|
4322
|
+
};
|
|
4323
|
+
};
|
|
4324
|
+
//#endregion
|
|
4325
|
+
//#region ../../packages/server/dist/insights/routes.mjs
|
|
4326
|
+
const json = (body, status) => Response.json(body, {
|
|
4327
|
+
headers: { "cache-control": "private, no-store" },
|
|
4328
|
+
status
|
|
4329
|
+
});
|
|
4330
|
+
const requireParam = (params, key) => {
|
|
4331
|
+
const value = params[key];
|
|
4332
|
+
if (value === void 0 || value.length === 0) throw new InsightsBadRequestError(`Missing route parameter: ${key}`);
|
|
4333
|
+
try {
|
|
4334
|
+
return decodeURIComponent(value);
|
|
4335
|
+
} catch {
|
|
4336
|
+
throw new InsightsBadRequestError(`Invalid route parameter: ${key}`);
|
|
4337
|
+
}
|
|
4338
|
+
};
|
|
4339
|
+
const run = async (operation) => {
|
|
4340
|
+
try {
|
|
4341
|
+
return await operation();
|
|
4342
|
+
} catch (error) {
|
|
4343
|
+
if (error instanceof InsightsBadRequestError) return json({ error: error.message }, 400);
|
|
4344
|
+
if (error instanceof InsightsPayloadTooLargeError) return json({ error: error.message }, 413);
|
|
4345
|
+
throw error;
|
|
4346
|
+
}
|
|
4347
|
+
};
|
|
4348
|
+
const query = (operation) => run(async () => json(await operation(), 200));
|
|
4349
|
+
const createInsightsRouteHandlers = (provider) => ({
|
|
4350
|
+
appendBundleEvent: async (_params, request) => run(async () => {
|
|
4351
|
+
await provider.appendBundleEvent(await parseBundleEventRequest(request));
|
|
4352
|
+
return new Response(null, { status: 204 });
|
|
4353
|
+
}),
|
|
4354
|
+
getReportingOverview: (_params, request) => query(() => provider.getReportingOverview(parseReportingOverviewInput(request))),
|
|
4355
|
+
getInstallation: (params) => run(async () => {
|
|
4356
|
+
const installation = await provider.getInstallation({ installId: requireParam(params, "installId") });
|
|
4357
|
+
return installation === null ? json({ error: "Installation not found" }, 404) : json(installation, 200);
|
|
4358
|
+
}),
|
|
4359
|
+
listEvents: (_params, request) => query(() => provider.listEvents(parseEventPageInput(request))),
|
|
4360
|
+
listInstallationEvents: (params, request) => query(() => provider.listInstallationEvents({
|
|
4361
|
+
...parseEventPageInput(request),
|
|
4362
|
+
installId: requireParam(params, "installId")
|
|
4363
|
+
})),
|
|
4364
|
+
pageInstallationsByCurrentUserId: (_params, request) => query(() => provider.pageInstallationsByCurrentUserId(parseUserInstallationPageInput(request)))
|
|
4365
|
+
});
|
|
4366
|
+
const registerInsightsClientRoutes = (add) => {
|
|
4367
|
+
add("POST", "/events", "appendBundleEvent");
|
|
4368
|
+
};
|
|
4369
|
+
const registerInsightsAdminRoutes = (add) => {
|
|
4370
|
+
add("GET", "/events", "listEvents");
|
|
4371
|
+
add("GET", "/overview", "getReportingOverview");
|
|
4372
|
+
add("GET", "/installations", "pageInstallationsByCurrentUserId");
|
|
4373
|
+
add("GET", "/installations/:installId/events", "listInstallationEvents");
|
|
4374
|
+
add("GET", "/installations/:installId", "getInstallation");
|
|
4375
|
+
};
|
|
4376
|
+
//#endregion
|
|
3969
4377
|
//#region ../../packages/server/dist/internalRouter.mjs
|
|
3970
4378
|
const normalizePath = (path) => {
|
|
3971
4379
|
if (!path) return "/";
|
|
@@ -4072,13 +4480,13 @@ const createDownloadStorageRouteHandler = (downloadStorageObject) => async (para
|
|
|
4072
4480
|
statusText: response.statusText
|
|
4073
4481
|
});
|
|
4074
4482
|
};
|
|
4075
|
-
function createHotUpdaterHandlers(api,
|
|
4483
|
+
function createHotUpdaterHandlers(api, insights, apiKeyAuth, downloadStorageObject) {
|
|
4076
4484
|
const routeHandlers = {
|
|
4077
4485
|
...createVersionRouteHandlers(),
|
|
4078
4486
|
...createReleaseCatalogRouteHandlers(apiKeyAuth?.headerName),
|
|
4079
4487
|
...createReleaseManagementRouteHandlers(),
|
|
4080
4488
|
...createBundleRouteHandlers(),
|
|
4081
|
-
...
|
|
4489
|
+
...insights === void 0 ? {} : createInsightsRouteHandlers(insights),
|
|
4082
4490
|
...downloadStorageObject === void 0 ? {} : { downloadStorageObject: createDownloadStorageRouteHandler(downloadStorageObject) }
|
|
4083
4491
|
};
|
|
4084
4492
|
const clientRouter = createRouter();
|
|
@@ -4088,7 +4496,7 @@ function createHotUpdaterHandlers(api, analytics, apiKeyAuth, downloadStorageObj
|
|
|
4088
4496
|
addClientRoute("GET", "/release-catalogs/app-version/:platform/:channelKey/:appVersion", "appVersionReleaseCatalog");
|
|
4089
4497
|
addClientRoute("GET", "/release-catalogs/fingerprint/:platform/:channelKey/:fingerprintHash", "fingerprintReleaseCatalog");
|
|
4090
4498
|
addClientRoute("GET", "/artifacts/:targetBundleId/from/:currentBundleId", "artifact");
|
|
4091
|
-
if (
|
|
4499
|
+
if (insights !== void 0) registerInsightsClientRoutes(addClientRoute);
|
|
4092
4500
|
const adminRouter = createRouter();
|
|
4093
4501
|
const addAdminRoute = (method, path, handler) => addRoute(adminRouter, method, path, handler);
|
|
4094
4502
|
addAdminRoute("GET", "/releases/:id", "getRelease");
|
|
@@ -4108,7 +4516,7 @@ function createHotUpdaterHandlers(api, analytics, apiKeyAuth, downloadStorageObj
|
|
|
4108
4516
|
addAdminRoute("POST", "/bundles", "createBundles");
|
|
4109
4517
|
addAdminRoute("PATCH", "/bundles/:id", "updateBundle");
|
|
4110
4518
|
addAdminRoute("DELETE", "/bundles/:id", "deleteBundle");
|
|
4111
|
-
if (
|
|
4519
|
+
if (insights !== void 0) registerInsightsAdminRoutes(addAdminRoute);
|
|
4112
4520
|
return Object.freeze({
|
|
4113
4521
|
client: createRequestHandler({
|
|
4114
4522
|
api,
|
|
@@ -4125,461 +4533,311 @@ function createHotUpdaterHandlers(api, analytics, apiKeyAuth, downloadStorageObj
|
|
|
4125
4533
|
});
|
|
4126
4534
|
}
|
|
4127
4535
|
//#endregion
|
|
4128
|
-
//#region ../../packages/server/dist/
|
|
4129
|
-
const
|
|
4130
|
-
const
|
|
4131
|
-
const
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
"
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
"24h": {
|
|
4139
|
-
bucketCount: 24,
|
|
4140
|
-
bucketSizeMs: HOUR_MS
|
|
4141
|
-
},
|
|
4142
|
-
"7d": {
|
|
4143
|
-
bucketCount: 7,
|
|
4144
|
-
bucketSizeMs: DAY_MS
|
|
4145
|
-
},
|
|
4146
|
-
"30d": {
|
|
4147
|
-
bucketCount: 30,
|
|
4148
|
-
bucketSizeMs: DAY_MS
|
|
4149
|
-
}
|
|
4536
|
+
//#region ../../packages/server/dist/insights/provider.mjs
|
|
4537
|
+
const DEFAULT_PAGE_LIMIT = 50;
|
|
4538
|
+
const MAX_PAGE_LIMIT = 100;
|
|
4539
|
+
const MAX_EVENT_ID_LENGTH = 1024;
|
|
4540
|
+
const MAX_IDENTITY_LENGTH = 255;
|
|
4541
|
+
const MAX_CURSOR_LENGTH = 8192;
|
|
4542
|
+
const WINDOW_MS = {
|
|
4543
|
+
"24h": 864e5,
|
|
4544
|
+
"7d": 6048e5,
|
|
4545
|
+
"30d": 2592e6
|
|
4150
4546
|
};
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
}
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
const
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4547
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4548
|
+
const requireString = (value, label, maximumLength) => {
|
|
4549
|
+
if (typeof value !== "string" || value.length === 0 || value.length > maximumLength || new TextDecoder("utf-8", { ignoreBOM: true }).decode(new TextEncoder().encode(value)) !== value) throw new InsightsBadRequestError(`Invalid ${label}.`);
|
|
4550
|
+
return value;
|
|
4551
|
+
};
|
|
4552
|
+
const requireTimestamp = (value, label) => {
|
|
4553
|
+
if (!Number.isSafeInteger(value) || Number(value) < 0) throw new InsightsBadRequestError(`Invalid ${label}.`);
|
|
4554
|
+
return Number(value);
|
|
4555
|
+
};
|
|
4556
|
+
const readLimit = (value) => {
|
|
4557
|
+
const limit = value ?? DEFAULT_PAGE_LIMIT;
|
|
4558
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_PAGE_LIMIT) throw new InsightsBadRequestError("Invalid page limit.");
|
|
4559
|
+
return limit;
|
|
4560
|
+
};
|
|
4561
|
+
const encodeBase64Url = (value) => {
|
|
4562
|
+
const bytes = new TextEncoder().encode(value);
|
|
4563
|
+
let binary = "";
|
|
4564
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
4565
|
+
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
|
|
4566
|
+
};
|
|
4567
|
+
const decodeBase64Url = (value) => {
|
|
4568
|
+
if (value.length === 0 || value.length > MAX_CURSOR_LENGTH) throw new InsightsBadRequestError("Invalid Insights cursor.");
|
|
4569
|
+
try {
|
|
4570
|
+
const base64 = value.replaceAll("-", "+").replaceAll("_", "/");
|
|
4571
|
+
const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
|
|
4572
|
+
const binary = atob(padded);
|
|
4573
|
+
const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
4574
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
4575
|
+
} catch {
|
|
4576
|
+
throw new InsightsBadRequestError("Invalid Insights cursor.");
|
|
4170
4577
|
}
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
if (
|
|
4178
|
-
|
|
4179
|
-
if (bucket === void 0) continue;
|
|
4180
|
-
const current = bucket.get(row.install_id);
|
|
4181
|
-
if (current === void 0 || isNewer(row, current)) bucket.set(row.install_id, row);
|
|
4578
|
+
};
|
|
4579
|
+
const encodeCursor = (value) => encodeBase64Url(JSON.stringify(value));
|
|
4580
|
+
const decodeCursor = (value) => {
|
|
4581
|
+
try {
|
|
4582
|
+
return JSON.parse(decodeBase64Url(value));
|
|
4583
|
+
} catch (error) {
|
|
4584
|
+
if (error instanceof InsightsBadRequestError) throw error;
|
|
4585
|
+
throw new InsightsBadRequestError("Invalid Insights cursor.");
|
|
4182
4586
|
}
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
return counts;
|
|
4187
|
-
});
|
|
4188
|
-
const bundleObservationTotals = /* @__PURE__ */ new Map();
|
|
4189
|
-
for (const counts of bundleCountsByBucket) for (const [bundleId, count] of counts) bundleObservationTotals.set(bundleId, (bundleObservationTotals.get(bundleId) ?? 0) + count);
|
|
4587
|
+
};
|
|
4588
|
+
const readScope = (input) => {
|
|
4589
|
+
if (input.platform !== "ios" && input.platform !== "android") throw new InsightsBadRequestError("Invalid Insights platform.");
|
|
4190
4590
|
return {
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
activeInstallations: selectedInstallIds.size,
|
|
4194
|
-
series: latestByBucket.map((bucket, index) => ({
|
|
4195
|
-
bucketStartMs: windowStartMs + index * definition.bucketSizeMs,
|
|
4196
|
-
value: bucket.size
|
|
4197
|
-
})),
|
|
4198
|
-
bundleSeries: [...bundleObservationTotals].sort(([leftId, leftTotal], [rightId, rightTotal]) => rightTotal - leftTotal || compareCodePoints$1(leftId, rightId)).map(([bundleId]) => ({
|
|
4199
|
-
bundleId,
|
|
4200
|
-
series: bundleCountsByBucket.map((counts, index) => ({
|
|
4201
|
-
bucketStartMs: windowStartMs + index * definition.bucketSizeMs,
|
|
4202
|
-
value: counts.get(bundleId) ?? 0
|
|
4203
|
-
}))
|
|
4204
|
-
})),
|
|
4205
|
-
bundles: [...bundleCounts].map(([bundleId, installations]) => ({
|
|
4206
|
-
bundleId,
|
|
4207
|
-
installations
|
|
4208
|
-
})).sort((left, right) => right.installations - left.installations || compareCodePoints$1(left.bundleId, right.bundleId))
|
|
4209
|
-
};
|
|
4210
|
-
}
|
|
4211
|
-
//#endregion
|
|
4212
|
-
//#region ../../packages/server/dist/analytics/bounded/scan.mjs
|
|
4213
|
-
const ANALYTICS_SCAN_MAX_ROWS = 5e4;
|
|
4214
|
-
const ANALYTICS_MATERIALIZATION_LIMIT = ANALYTICS_SCAN_MAX_ROWS + 1;
|
|
4215
|
-
const ANALYTICS_SCAN_PAGE_SIZE = 1e3;
|
|
4216
|
-
const ANALYTICS_LOWER_BOUND_ID = "00000000-0000-0000-0000-000000000000";
|
|
4217
|
-
const compareCodePoints = (left, right) => {
|
|
4218
|
-
if (left < right) return -1;
|
|
4219
|
-
if (left > right) return 1;
|
|
4220
|
-
return 0;
|
|
4221
|
-
};
|
|
4222
|
-
const compareEventNewest = (left, right) => right.received_at_ms - left.received_at_ms || compareCodePoints(right.id, left.id);
|
|
4223
|
-
const compareEventOldest = (left, right) => left.received_at_ms - right.received_at_ms || compareCodePoints(left.id, right.id);
|
|
4224
|
-
const materializeEventRows = async (scope) => {
|
|
4225
|
-
const rows = [];
|
|
4226
|
-
const seenIds = /* @__PURE__ */ new Set();
|
|
4227
|
-
let after = scope.lowerBoundMs === void 0 ? void 0 : {
|
|
4228
|
-
receivedAtMs: scope.lowerBoundMs,
|
|
4229
|
-
id: ANALYTICS_LOWER_BOUND_ID
|
|
4591
|
+
platform: input.platform,
|
|
4592
|
+
channel: requireString(input.channel, "channel", MAX_EVENT_ID_LENGTH)
|
|
4230
4593
|
};
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
let previous = after === void 0 ? void 0 : {
|
|
4241
|
-
received_at_ms: after.receivedAtMs,
|
|
4242
|
-
id: after.id
|
|
4594
|
+
};
|
|
4595
|
+
const bundleFilter = (input) => {
|
|
4596
|
+
const scope = readScope(input);
|
|
4597
|
+
const bundleId = requireString(input.bundleId, "bundle ID", MAX_EVENT_ID_LENGTH);
|
|
4598
|
+
switch (input.outcome) {
|
|
4599
|
+
case "applied": return {
|
|
4600
|
+
...scope,
|
|
4601
|
+
type: "UPDATE_APPLIED",
|
|
4602
|
+
toBundleId: bundleId
|
|
4243
4603
|
};
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
previous = row;
|
|
4249
|
-
}
|
|
4250
|
-
const last = page.at(-1);
|
|
4251
|
-
if (last !== void 0) after = {
|
|
4252
|
-
receivedAtMs: last.received_at_ms,
|
|
4253
|
-
id: last.id
|
|
4604
|
+
case "recovered": return {
|
|
4605
|
+
...scope,
|
|
4606
|
+
type: "RECOVERED",
|
|
4607
|
+
fromBundleId: bundleId
|
|
4254
4608
|
};
|
|
4255
|
-
|
|
4609
|
+
case "adopted": return {
|
|
4610
|
+
...scope,
|
|
4611
|
+
type: "RELEASE_ADOPTED",
|
|
4612
|
+
toBundleId: bundleId
|
|
4613
|
+
};
|
|
4614
|
+
default: throw new InsightsBadRequestError("Invalid Insights outcome.");
|
|
4256
4615
|
}
|
|
4257
|
-
return rows;
|
|
4258
4616
|
};
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4617
|
+
const sameFilter = (left, right) => {
|
|
4618
|
+
if (!isRecord(left) || left.kind !== right.kind) return false;
|
|
4619
|
+
switch (right.kind) {
|
|
4620
|
+
case "all": return true;
|
|
4621
|
+
case "installationMovement": return left.installId === right.installId;
|
|
4622
|
+
case "bundle": return left.platform === right.platform && left.channel === right.channel && left.type === right.type && (right.type === "RECOVERED" ? left.fromBundleId === right.fromBundleId : left.toBundleId === right.toBundleId);
|
|
4263
4623
|
}
|
|
4264
4624
|
};
|
|
4265
|
-
const
|
|
4266
|
-
const
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
lowerBoundMs: scope.cutoffMs - durationMs
|
|
4271
|
-
})).filter((row) => row.received_at_ms >= scope.cutoffMs - durationMs && ACTIVE_BUNDLE_EVENT_TYPES.includes(row.type));
|
|
4272
|
-
};
|
|
4273
|
-
const startOfUtcHour = (value) => {
|
|
4274
|
-
const date = new Date(value);
|
|
4275
|
-
return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours());
|
|
4276
|
-
};
|
|
4277
|
-
const startOfUtcDay = (value) => {
|
|
4278
|
-
const date = new Date(value);
|
|
4279
|
-
return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
|
|
4280
|
-
};
|
|
4281
|
-
const getWindowRange = (window, now) => {
|
|
4282
|
-
if (window === "24h") return {
|
|
4283
|
-
sizeMs: 3600 * 1e3,
|
|
4284
|
-
rangeStart: startOfUtcHour(now) - 1380 * 60 * 1e3
|
|
4285
|
-
};
|
|
4286
|
-
const days = window === "7d" ? 7 : 30;
|
|
4625
|
+
const readEventCursor = (value, filter) => {
|
|
4626
|
+
const cursor = decodeCursor(value);
|
|
4627
|
+
if (!isRecord(cursor) || cursor.version !== 2 || cursor.kind !== "events" || !isRecord(cursor.filter) || !isRecord(cursor.after)) throw new InsightsBadRequestError("Invalid Insights cursor.");
|
|
4628
|
+
if (!sameFilter(cursor.filter, filter)) throw new InsightsBadRequestError("Insights cursor does not match the requested events.");
|
|
4629
|
+
if (typeof cursor.after.id !== "string" || !isUUIDv7(cursor.after.id)) throw new InsightsBadRequestError("Invalid Insights event cursor ID.");
|
|
4287
4630
|
return {
|
|
4288
|
-
|
|
4289
|
-
|
|
4631
|
+
after: {
|
|
4632
|
+
id: cursor.after.id,
|
|
4633
|
+
receivedAtMs: requireTimestamp(cursor.after.receivedAtMs, "event cursor")
|
|
4634
|
+
},
|
|
4635
|
+
beforeReceivedAtMs: requireTimestamp(cursor.beforeReceivedAtMs, "event cutoff"),
|
|
4636
|
+
kind: "events",
|
|
4637
|
+
sinceMs: requireTimestamp(cursor.sinceMs, "event start"),
|
|
4638
|
+
filter,
|
|
4639
|
+
version: 2
|
|
4290
4640
|
};
|
|
4291
4641
|
};
|
|
4292
|
-
const
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
return (await materializeEventRows({
|
|
4296
|
-
...scope,
|
|
4297
|
-
lowerBoundMs: range.rangeStart
|
|
4298
|
-
})).filter(({ received_at_ms }) => received_at_ms >= range.rangeStart);
|
|
4299
|
-
};
|
|
4300
|
-
const bucketStart = (receivedAtMs, sizeMs) => sizeMs === 3600 * 1e3 ? startOfUtcHour(receivedAtMs) : startOfUtcDay(receivedAtMs);
|
|
4301
|
-
const createSeries = (request) => {
|
|
4302
|
-
const range = request.window === "all" ? void 0 : getWindowRange(request.window, request.cutoffMs);
|
|
4303
|
-
const sizeMs = range?.sizeMs ?? 1440 * 60 * 1e3;
|
|
4304
|
-
const installIdsByBucket = /* @__PURE__ */ new Map();
|
|
4305
|
-
let oldestMs = request.cutoffMs;
|
|
4306
|
-
for (const row of request.rows) {
|
|
4307
|
-
oldestMs = Math.min(oldestMs, row.received_at_ms);
|
|
4308
|
-
const start = bucketStart(row.received_at_ms, sizeMs);
|
|
4309
|
-
const installIds = installIdsByBucket.get(start) ?? /* @__PURE__ */ new Set();
|
|
4310
|
-
installIds.add(row.install_id);
|
|
4311
|
-
installIdsByBucket.set(start, installIds);
|
|
4312
|
-
}
|
|
4313
|
-
const first = range?.rangeStart ?? startOfUtcDay(oldestMs);
|
|
4314
|
-
const last = bucketStart(request.cutoffMs, sizeMs);
|
|
4315
|
-
return Array.from({ length: Math.floor((last - first) / sizeMs) + 1 }, (_, index) => {
|
|
4316
|
-
const start = first + index * sizeMs;
|
|
4317
|
-
return {
|
|
4318
|
-
bucketStartMs: start,
|
|
4319
|
-
value: installIdsByBucket.get(start)?.size ?? 0
|
|
4320
|
-
};
|
|
4321
|
-
});
|
|
4322
|
-
};
|
|
4323
|
-
const collectEventActivity = (request) => {
|
|
4324
|
-
const range = request.window === "all" ? void 0 : getWindowRange(request.window, request.cutoffMs);
|
|
4325
|
-
const rows = range === void 0 ? request.rows : request.rows.filter(({ received_at_ms }) => received_at_ms >= range.rangeStart && received_at_ms < request.cutoffMs);
|
|
4326
|
-
const installs = /* @__PURE__ */ new Set();
|
|
4327
|
-
const installsByCohort = /* @__PURE__ */ new Map();
|
|
4328
|
-
for (const row of rows) {
|
|
4329
|
-
installs.add(row.install_id);
|
|
4330
|
-
const cohort = installsByCohort.get(row.cohort) ?? /* @__PURE__ */ new Set();
|
|
4331
|
-
cohort.add(row.install_id);
|
|
4332
|
-
installsByCohort.set(row.cohort, cohort);
|
|
4333
|
-
}
|
|
4642
|
+
const readUserInstallationCursor = (value, userId) => {
|
|
4643
|
+
const cursor = decodeCursor(value);
|
|
4644
|
+
if (!isRecord(cursor) || cursor.version !== 1 || cursor.kind !== "user-installations" || cursor.userId !== userId) throw new InsightsBadRequestError("Insights cursor does not match the requested user ID.");
|
|
4334
4645
|
return {
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
})),
|
|
4340
|
-
series: createSeries({
|
|
4341
|
-
...request,
|
|
4342
|
-
rows
|
|
4343
|
-
})
|
|
4646
|
+
afterInstallId: requireString(cursor.afterInstallId, "installation cursor", MAX_IDENTITY_LENGTH),
|
|
4647
|
+
kind: "user-installations",
|
|
4648
|
+
userId,
|
|
4649
|
+
version: 1
|
|
4344
4650
|
};
|
|
4345
4651
|
};
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
latestStatus: row.type,
|
|
4355
|
-
platform: row.platform,
|
|
4356
|
-
appVersion: row.app_version,
|
|
4357
|
-
channel: row.channel,
|
|
4358
|
-
cohort: row.cohort,
|
|
4359
|
-
receivedAtMs: row.received_at_ms
|
|
4360
|
-
};
|
|
4361
|
-
}
|
|
4362
|
-
function matchesIdentity(row, query) {
|
|
4363
|
-
return row.install_id.toLowerCase().includes(query) || row.user_id?.toLowerCase().includes(query) === true || row.username?.toLowerCase().includes(query) === true;
|
|
4364
|
-
}
|
|
4365
|
-
function searchEventInstallations(request) {
|
|
4366
|
-
const query = request.query.toLowerCase();
|
|
4367
|
-
const matchingInstallIds = /* @__PURE__ */ new Set();
|
|
4368
|
-
for (const row of request.rows) if (query.length === 0 || matchesIdentity(row, query)) matchingInstallIds.add(row.install_id);
|
|
4369
|
-
const latestByInstall = /* @__PURE__ */ new Map();
|
|
4370
|
-
for (const row of request.rows) {
|
|
4371
|
-
if (!matchingInstallIds.has(row.install_id)) continue;
|
|
4372
|
-
const current = latestByInstall.get(row.install_id);
|
|
4373
|
-
if (current === void 0 || compareEventNewest(row, current) < 0) latestByInstall.set(row.install_id, row);
|
|
4652
|
+
const compareEventNewest = (left, right) => right.received_at_ms - left.received_at_ms || compareInsightsText(right.id, left.id);
|
|
4653
|
+
const isAfterEventCursor = (row, after) => row.received_at_ms < after.receivedAtMs || row.received_at_ms === after.receivedAtMs && compareInsightsText(row.id, after.id) < 0;
|
|
4654
|
+
const assertEventRows = (rows, input) => {
|
|
4655
|
+
if (rows.length > input.limit) throw new Error("Insights database returned too many event rows.");
|
|
4656
|
+
for (let index = 0; index < rows.length; index += 1) {
|
|
4657
|
+
const row = rows[index];
|
|
4658
|
+
const previous = rows[index - 1];
|
|
4659
|
+
if (!row || typeof row.id !== "string" || !isUUIDv7(row.id) || !Number.isSafeInteger(row.received_at_ms) || row.received_at_ms >= input.beforeReceivedAtMs || row.received_at_ms < input.sinceMs || previous !== void 0 && compareEventNewest(previous, row) >= 0 || input.after !== void 0 && !isAfterEventCursor(row, input.after) || input.filter.kind === "installationMovement" && (row.install_id !== input.filter.installId || !isInsightsMovementEvent(row)) || input.filter.kind === "bundle" && (row.platform !== input.filter.platform || row.channel !== input.filter.channel || row.type !== input.filter.type || (input.filter.type === "RECOVERED" ? row.from_bundle_id !== input.filter.fromBundleId : row.to_bundle_id !== input.filter.toBundleId))) throw new Error("Insights database returned invalid event rows.");
|
|
4374
4660
|
}
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
total: matchingRows.length,
|
|
4381
|
-
limit: request.limit,
|
|
4382
|
-
offset: request.offset
|
|
4383
|
-
}
|
|
4384
|
-
};
|
|
4385
|
-
}
|
|
4386
|
-
//#endregion
|
|
4387
|
-
//#region ../../packages/server/dist/analytics/bounded/persistence.mjs
|
|
4388
|
-
function createBundleEventRow(input) {
|
|
4389
|
-
const base = {
|
|
4390
|
-
id: createUUIDv7(),
|
|
4391
|
-
install_id: input.installId,
|
|
4392
|
-
user_id: input.userId ?? null,
|
|
4393
|
-
username: input.username ?? null,
|
|
4394
|
-
from_release_id: input.fromReleaseId,
|
|
4395
|
-
to_release_id: input.toReleaseId,
|
|
4396
|
-
to_bundle_id: input.toBundleId,
|
|
4397
|
-
platform: input.platform,
|
|
4398
|
-
app_version: input.appVersion,
|
|
4399
|
-
channel: input.channel,
|
|
4400
|
-
cohort: input.cohort,
|
|
4401
|
-
fingerprint_hash: input.fingerprintHash,
|
|
4402
|
-
sdk_version: input.sdkVersion ?? null,
|
|
4403
|
-
received_at_ms: Date.now()
|
|
4404
|
-
};
|
|
4405
|
-
switch (input.type) {
|
|
4406
|
-
case "UPDATE_APPLIED":
|
|
4407
|
-
case "RECOVERED":
|
|
4408
|
-
case "RELEASE_ADOPTED": return {
|
|
4409
|
-
...base,
|
|
4410
|
-
type: input.type,
|
|
4411
|
-
from_bundle_id: input.fromBundleId,
|
|
4412
|
-
update_strategy: input.updateStrategy
|
|
4413
|
-
};
|
|
4414
|
-
case "UNCHANGED": return {
|
|
4415
|
-
...base,
|
|
4416
|
-
type: input.type,
|
|
4417
|
-
from_bundle_id: null,
|
|
4418
|
-
update_strategy: null
|
|
4419
|
-
};
|
|
4420
|
-
}
|
|
4421
|
-
}
|
|
4422
|
-
//#endregion
|
|
4423
|
-
//#region ../../packages/server/dist/analytics/bounded/provider.mjs
|
|
4424
|
-
const isTransitionEventRow = (row) => row.type === "UPDATE_APPLIED" || row.type === "RECOVERED";
|
|
4425
|
-
const toHistoryRow = (row) => ({
|
|
4426
|
-
id: row.id,
|
|
4427
|
-
type: row.type,
|
|
4661
|
+
};
|
|
4662
|
+
const toEventHistoryRow = (row) => ({
|
|
4663
|
+
appVersion: row.app_version,
|
|
4664
|
+
channel: row.channel,
|
|
4665
|
+
cohort: row.cohort,
|
|
4428
4666
|
fromBundleId: row.from_bundle_id,
|
|
4667
|
+
id: row.id,
|
|
4668
|
+
installId: row.install_id,
|
|
4669
|
+
platform: row.platform,
|
|
4670
|
+
receivedAtMs: row.received_at_ms,
|
|
4429
4671
|
toBundleId: row.to_bundle_id,
|
|
4430
|
-
|
|
4672
|
+
type: row.type,
|
|
4431
4673
|
userId: row.user_id,
|
|
4432
|
-
|
|
4674
|
+
username: row.username
|
|
4675
|
+
});
|
|
4676
|
+
const toInstallationRow = (row) => ({
|
|
4433
4677
|
appVersion: row.app_version,
|
|
4434
4678
|
channel: row.channel,
|
|
4435
4679
|
cohort: row.cohort,
|
|
4436
|
-
|
|
4680
|
+
installId: row.install_id,
|
|
4681
|
+
lastKnownBundleId: row.to_bundle_id,
|
|
4682
|
+
latestStatus: row.type,
|
|
4683
|
+
platform: row.platform,
|
|
4684
|
+
receivedAtMs: row.received_at_ms,
|
|
4685
|
+
userId: row.user_id,
|
|
4686
|
+
username: row.username
|
|
4437
4687
|
});
|
|
4438
|
-
const
|
|
4439
|
-
const
|
|
4440
|
-
const
|
|
4441
|
-
const
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4688
|
+
const pageEventRows = async (model, input, filter, map) => {
|
|
4689
|
+
const limit = readLimit(input.limit);
|
|
4690
|
+
const cursor = input.cursor === void 0 ? void 0 : readEventCursor(input.cursor, filter);
|
|
4691
|
+
const beforeReceivedAtMs = cursor?.beforeReceivedAtMs ?? (input.beforeReceivedAtMs === void 0 ? Date.now() : requireTimestamp(input.beforeReceivedAtMs, "event cutoff"));
|
|
4692
|
+
if (cursor !== void 0 && input.beforeReceivedAtMs !== void 0 && input.beforeReceivedAtMs !== beforeReceivedAtMs) throw new InsightsBadRequestError("Insights cursor does not match the requested event cutoff.");
|
|
4693
|
+
const sinceMs = cursor?.sinceMs ?? (input.sinceMs === void 0 ? 0 : requireTimestamp(input.sinceMs, "event start"));
|
|
4694
|
+
if (sinceMs > beforeReceivedAtMs || input.sinceMs !== void 0 && input.sinceMs !== sinceMs || cursor !== void 0 && (cursor.after.receivedAtMs < sinceMs || cursor.after.receivedAtMs >= beforeReceivedAtMs)) throw new InsightsBadRequestError("Insights cursor or range does not match the requested event start.");
|
|
4695
|
+
const databaseInput = {
|
|
4696
|
+
filter,
|
|
4697
|
+
sinceMs,
|
|
4698
|
+
beforeReceivedAtMs,
|
|
4699
|
+
...cursor === void 0 ? {} : { after: cursor.after },
|
|
4700
|
+
limit: limit + 1
|
|
4445
4701
|
};
|
|
4446
|
-
const rows =
|
|
4447
|
-
|
|
4448
|
-
const
|
|
4449
|
-
const
|
|
4450
|
-
rows: installedRows,
|
|
4451
|
-
window,
|
|
4452
|
-
cutoffMs: scope.cutoffMs
|
|
4453
|
-
});
|
|
4454
|
-
const recovered = collectEventActivity({
|
|
4455
|
-
rows: recoveredRows,
|
|
4456
|
-
window,
|
|
4457
|
-
cutoffMs: scope.cutoffMs
|
|
4458
|
-
});
|
|
4459
|
-
const recentRows = [...installedRows, ...recoveredRows].sort(compareEventNewest);
|
|
4702
|
+
const rows = await model.listEvents(databaseInput);
|
|
4703
|
+
assertEventRows(rows, databaseInput);
|
|
4704
|
+
const pageRows = rows.slice(0, limit);
|
|
4705
|
+
const last = pageRows.at(-1);
|
|
4460
4706
|
return {
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
data: recentRows.slice(offset, offset + limit).map(toHistoryRow),
|
|
4475
|
-
pagination: {
|
|
4476
|
-
total: recentRows.length,
|
|
4477
|
-
limit,
|
|
4478
|
-
offset
|
|
4479
|
-
}
|
|
4480
|
-
}
|
|
4707
|
+
beforeReceivedAtMs,
|
|
4708
|
+
data: pageRows.map(map),
|
|
4709
|
+
nextCursor: rows.length > limit && last ? encodeCursor({
|
|
4710
|
+
after: {
|
|
4711
|
+
id: last.id,
|
|
4712
|
+
receivedAtMs: last.received_at_ms
|
|
4713
|
+
},
|
|
4714
|
+
beforeReceivedAtMs,
|
|
4715
|
+
kind: "events",
|
|
4716
|
+
filter,
|
|
4717
|
+
sinceMs,
|
|
4718
|
+
version: 2
|
|
4719
|
+
}) : null
|
|
4481
4720
|
};
|
|
4482
4721
|
};
|
|
4483
|
-
const
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
const rows = (await materializeRowsForWindow({
|
|
4490
|
-
persistence,
|
|
4491
|
-
cutoffMs: Date.now()
|
|
4492
|
-
}, window)).filter(isTransitionEventRow);
|
|
4493
|
-
for (const row of rows) {
|
|
4494
|
-
const bundleId = row.type === "UPDATE_APPLIED" ? row.to_bundle_id : row.from_bundle_id;
|
|
4495
|
-
if (!requestedBundleIds.has(bundleId)) continue;
|
|
4496
|
-
const counts = row.type === "UPDATE_APPLIED" ? installedByBundleId : recoveredByBundleId;
|
|
4497
|
-
const installIds = counts.get(bundleId) ?? /* @__PURE__ */ new Set();
|
|
4498
|
-
installIds.add(row.install_id);
|
|
4499
|
-
counts.set(bundleId, installIds);
|
|
4722
|
+
const assertInstallationRows = (rows, input) => {
|
|
4723
|
+
if (rows.length > input.limit) throw new Error("Insights database returned too many installation rows.");
|
|
4724
|
+
for (let index = 0; index < rows.length; index += 1) {
|
|
4725
|
+
const row = rows[index];
|
|
4726
|
+
const previous = rows[index - 1];
|
|
4727
|
+
if (!row || row.user_id !== input.userId || input.afterInstallId !== void 0 && compareInsightsText(row.install_id, input.afterInstallId) <= 0 || previous !== void 0 && compareInsightsText(previous.install_id, row.install_id) >= 0) throw new Error("Insights database returned invalid installation rows.");
|
|
4500
4728
|
}
|
|
4501
|
-
return normalizedBundleIds.map((bundleId) => ({
|
|
4502
|
-
bundleId,
|
|
4503
|
-
installed: installedByBundleId.get(bundleId)?.size ?? 0,
|
|
4504
|
-
recovered: recoveredByBundleId.get(bundleId)?.size ?? 0
|
|
4505
|
-
}));
|
|
4506
4729
|
};
|
|
4507
|
-
const
|
|
4508
|
-
mode: "bounded",
|
|
4509
|
-
maxMatchingRows: ANALYTICS_SCAN_MAX_ROWS,
|
|
4730
|
+
const createInsightsProvider = (model) => Object.freeze({
|
|
4510
4731
|
async appendBundleEvent(input) {
|
|
4511
|
-
|
|
4732
|
+
const event = createBundleEventRow(input);
|
|
4733
|
+
await model.record({
|
|
4734
|
+
event,
|
|
4735
|
+
installation: toInsightsInstallationRow(event)
|
|
4736
|
+
});
|
|
4512
4737
|
},
|
|
4513
|
-
|
|
4514
|
-
const
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
})).filter(isTransitionEventRow);
|
|
4518
|
-
return {
|
|
4519
|
-
installed: countDistinctInstallations(rows.filter((row) => isInstalledForBundle(row, bundleId))),
|
|
4520
|
-
recovered: countDistinctInstallations(rows.filter((row) => isRecoveredFromBundle(row, bundleId)))
|
|
4738
|
+
listEvents(input) {
|
|
4739
|
+
const filter = input.bundle === void 0 ? { kind: "all" } : {
|
|
4740
|
+
kind: "bundle",
|
|
4741
|
+
...bundleFilter(input.bundle)
|
|
4521
4742
|
};
|
|
4743
|
+
return pageEventRows(model, input, filter, toEventHistoryRow);
|
|
4522
4744
|
},
|
|
4523
|
-
|
|
4524
|
-
|
|
4745
|
+
async listInstallationEvents(input) {
|
|
4746
|
+
if ("bundle" in input && input.bundle !== void 0) throw new InsightsBadRequestError("Installation movement queries cannot include a bundle filter.");
|
|
4747
|
+
const installId = requireString(input.installId, "install ID", MAX_IDENTITY_LENGTH);
|
|
4748
|
+
return pageEventRows(model, input, {
|
|
4749
|
+
kind: "installationMovement",
|
|
4750
|
+
installId
|
|
4751
|
+
}, (row) => toEventHistoryRow(row));
|
|
4525
4752
|
},
|
|
4526
|
-
|
|
4527
|
-
|
|
4753
|
+
async getInstallation({ installId }) {
|
|
4754
|
+
const normalizedInstallId = requireString(installId, "install ID", MAX_IDENTITY_LENGTH);
|
|
4755
|
+
const rows = await model.findInstallations({ installId: normalizedInstallId });
|
|
4756
|
+
const row = rows[0] ?? null;
|
|
4757
|
+
if (rows.length > 1 || row !== null && row.install_id !== normalizedInstallId) throw new Error("Insights database returned an invalid installation.");
|
|
4758
|
+
return row === null ? null : toInstallationRow(row);
|
|
4528
4759
|
},
|
|
4529
|
-
async
|
|
4530
|
-
const
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4760
|
+
async pageInstallationsByCurrentUserId(input) {
|
|
4761
|
+
const userId = requireString(input.userId, "user ID", MAX_IDENTITY_LENGTH);
|
|
4762
|
+
const limit = readLimit(input.limit);
|
|
4763
|
+
const cursor = input.cursor === void 0 ? void 0 : readUserInstallationCursor(input.cursor, userId);
|
|
4764
|
+
const databaseInput = {
|
|
4765
|
+
userId,
|
|
4766
|
+
...cursor === void 0 ? {} : { afterInstallId: cursor.afterInstallId },
|
|
4767
|
+
limit: limit + 1
|
|
4768
|
+
};
|
|
4769
|
+
const rows = await model.findInstallations(databaseInput);
|
|
4770
|
+
assertInstallationRows(rows, databaseInput);
|
|
4771
|
+
const pageRows = rows.slice(0, limit);
|
|
4772
|
+
const last = pageRows.at(-1);
|
|
4541
4773
|
return {
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
installations
|
|
4546
|
-
|
|
4774
|
+
data: pageRows.map(toInstallationRow),
|
|
4775
|
+
nextCursor: rows.length > limit && last ? encodeCursor({
|
|
4776
|
+
afterInstallId: last.install_id,
|
|
4777
|
+
kind: "user-installations",
|
|
4778
|
+
userId,
|
|
4779
|
+
version: 1
|
|
4780
|
+
}) : null
|
|
4547
4781
|
};
|
|
4548
4782
|
},
|
|
4549
|
-
async
|
|
4550
|
-
const
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4783
|
+
async getReportingOverview(input) {
|
|
4784
|
+
const scope = readScope(input);
|
|
4785
|
+
const { window } = input;
|
|
4786
|
+
if (!Object.hasOwn(WINDOW_MS, window)) throw new InsightsBadRequestError("Invalid reporting installation window.");
|
|
4787
|
+
const beforeReceivedAtMs = Date.now();
|
|
4788
|
+
const sinceMs = Math.max(0, beforeReceivedAtMs - WINDOW_MS[window]);
|
|
4789
|
+
const measure = async (count) => {
|
|
4790
|
+
const value = await count;
|
|
4791
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new Error("Insights database returned an invalid count.");
|
|
4792
|
+
return {
|
|
4793
|
+
count: value,
|
|
4794
|
+
measuredAtMs: Date.now()
|
|
4795
|
+
};
|
|
4796
|
+
};
|
|
4797
|
+
const bundleId = input.bundleId === void 0 ? void 0 : requireString(input.bundleId, "bundle ID", MAX_EVENT_ID_LENGTH);
|
|
4798
|
+
const reporting = measure(model.countInstallations({
|
|
4799
|
+
...scope,
|
|
4800
|
+
sinceMs
|
|
4801
|
+
}));
|
|
4802
|
+
if (bundleId === void 0) return {
|
|
4803
|
+
...scope,
|
|
4804
|
+
window,
|
|
4805
|
+
sinceMs,
|
|
4806
|
+
beforeReceivedAtMs,
|
|
4807
|
+
reportingInstallations: await reporting
|
|
4808
|
+
};
|
|
4809
|
+
const countOutcome = (outcome) => measure(model.countEvents({
|
|
4810
|
+
filter: bundleFilter({
|
|
4811
|
+
...scope,
|
|
4812
|
+
bundleId,
|
|
4813
|
+
outcome
|
|
4566
4814
|
}),
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4815
|
+
sinceMs,
|
|
4816
|
+
beforeReceivedAtMs
|
|
4817
|
+
}));
|
|
4818
|
+
const [reportingInstallations, bundleInstallations, appliedReports, recoveredReports, adoptedReports] = await Promise.all([
|
|
4819
|
+
reporting,
|
|
4820
|
+
measure(model.countInstallations({
|
|
4821
|
+
...scope,
|
|
4822
|
+
sinceMs,
|
|
4823
|
+
bundleId
|
|
4824
|
+
})),
|
|
4825
|
+
countOutcome("applied"),
|
|
4826
|
+
countOutcome("recovered"),
|
|
4827
|
+
countOutcome("adopted")
|
|
4828
|
+
]);
|
|
4577
4829
|
return {
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4830
|
+
...scope,
|
|
4831
|
+
window,
|
|
4832
|
+
sinceMs,
|
|
4833
|
+
beforeReceivedAtMs,
|
|
4834
|
+
reportingInstallations,
|
|
4835
|
+
bundle: {
|
|
4836
|
+
bundleId,
|
|
4837
|
+
reportingInstallations: bundleInstallations,
|
|
4838
|
+
appliedReports,
|
|
4839
|
+
recoveredReports,
|
|
4840
|
+
adoptedReports
|
|
4583
4841
|
}
|
|
4584
4842
|
};
|
|
4585
4843
|
}
|
|
@@ -4620,7 +4878,7 @@ const hashApiKey = async (apiKey) => {
|
|
|
4620
4878
|
return bytesToBase64Url(new Uint8Array(digest));
|
|
4621
4879
|
};
|
|
4622
4880
|
const apiKeyId = () => {
|
|
4623
|
-
const bytes = new Uint8Array(16);
|
|
4881
|
+
const bytes = /* @__PURE__ */ new Uint8Array(16);
|
|
4624
4882
|
crypto.getRandomValues(bytes);
|
|
4625
4883
|
return `api-${bytesToBase64Url(bytes)}`;
|
|
4626
4884
|
};
|
|
@@ -4657,7 +4915,7 @@ const registerApiKey = async (input) => {
|
|
|
4657
4915
|
});
|
|
4658
4916
|
};
|
|
4659
4917
|
const createApiKey = (input) => {
|
|
4660
|
-
const bytes = new Uint8Array(32);
|
|
4918
|
+
const bytes = /* @__PURE__ */ new Uint8Array(32);
|
|
4661
4919
|
crypto.getRandomValues(bytes);
|
|
4662
4920
|
return registerApiKey({
|
|
4663
4921
|
apiKey: bytesToBase64Url(bytes),
|
|
@@ -5077,6 +5335,8 @@ const unsupportedSchemaUpgradeMessage = (version) => `Hot Updater v1 cannot migr
|
|
|
5077
5335
|
//#endregion
|
|
5078
5336
|
//#region ../../packages/server/dist/db/schemaReadiness.mjs
|
|
5079
5337
|
var HotUpdaterSchemaMigrationRequiredError = class extends Error {
|
|
5338
|
+
adapterName;
|
|
5339
|
+
currentVersion;
|
|
5080
5340
|
constructor(adapterName, currentVersion) {
|
|
5081
5341
|
super(currentVersion === void 0 ? `Hot Updater database schema is not initialized for ${adapterName}. Run \`hot-updater db migrate\` before using this adapter.` : unsupportedSchemaUpgradeMessage(currentVersion));
|
|
5082
5342
|
this.adapterName = adapterName;
|
|
@@ -5106,7 +5366,7 @@ const sqlProviders = [
|
|
|
5106
5366
|
const noSqlProviders = ["mongodb"];
|
|
5107
5367
|
[...sqlProviders, ...noSqlProviders];
|
|
5108
5368
|
function isDatabasePlugin(plugin) {
|
|
5109
|
-
return typeof plugin === "object" && plugin !== null && "name" in plugin && typeof plugin.name === "string" && "models" in plugin && typeof plugin.models === "object" && plugin.models !== null && "bundles" in plugin.models && typeof plugin.models.bundles === "object" && plugin.models.bundles !== null && "findById" in plugin.models.bundles && typeof plugin.models.bundles.findById === "function" && "findMany" in plugin.models.bundles && typeof plugin.models.bundles.findMany === "function" && "count" in plugin.models.bundles && typeof plugin.models.bundles.count === "function" && "bundlePatches" in plugin.models && typeof plugin.models.bundlePatches === "object" && plugin.models.bundlePatches !== null && "findByBundleIds" in plugin.models.bundlePatches && typeof plugin.models.bundlePatches.findByBundleIds === "function" && "channels" in plugin.models && typeof plugin.models.channels === "object" && plugin.models.channels !== null && "insert" in plugin.models.channels && typeof plugin.models.channels.insert === "function" && "list" in plugin.models.channels && typeof plugin.models.channels.list === "function" && "delete" in plugin.models.channels && typeof plugin.models.channels.delete === "function" && "
|
|
5369
|
+
return typeof plugin === "object" && plugin !== null && "name" in plugin && typeof plugin.name === "string" && "models" in plugin && typeof plugin.models === "object" && plugin.models !== null && "bundles" in plugin.models && typeof plugin.models.bundles === "object" && plugin.models.bundles !== null && "findById" in plugin.models.bundles && typeof plugin.models.bundles.findById === "function" && "findMany" in plugin.models.bundles && typeof plugin.models.bundles.findMany === "function" && "count" in plugin.models.bundles && typeof plugin.models.bundles.count === "function" && "bundlePatches" in plugin.models && typeof plugin.models.bundlePatches === "object" && plugin.models.bundlePatches !== null && "findByBundleIds" in plugin.models.bundlePatches && typeof plugin.models.bundlePatches.findByBundleIds === "function" && "channels" in plugin.models && typeof plugin.models.channels === "object" && plugin.models.channels !== null && "insert" in plugin.models.channels && typeof plugin.models.channels.insert === "function" && "list" in plugin.models.channels && typeof plugin.models.channels.list === "function" && "delete" in plugin.models.channels && typeof plugin.models.channels.delete === "function" && "insights" in plugin.models && typeof plugin.models.insights === "object" && plugin.models.insights !== null && "record" in plugin.models.insights && typeof plugin.models.insights.record === "function" && "listEvents" in plugin.models.insights && typeof plugin.models.insights.listEvents === "function" && "findInstallations" in plugin.models.insights && typeof plugin.models.insights.findInstallations === "function" && "countEvents" in plugin.models.insights && typeof plugin.models.insights.countEvents === "function" && "countInstallations" in plugin.models.insights && typeof plugin.models.insights.countInstallations === "function" && "apiKeys" in plugin.models && typeof plugin.models.apiKeys === "object" && plugin.models.apiKeys !== null && "create" in plugin.models.apiKeys && typeof plugin.models.apiKeys.create === "function" && "findByHash" in plugin.models.apiKeys && typeof plugin.models.apiKeys.findByHash === "function" && "list" in plugin.models.apiKeys && typeof plugin.models.apiKeys.list === "function" && "revoke" in plugin.models.apiKeys && typeof plugin.models.apiKeys.revoke === "function" && "commit" in plugin && typeof plugin.commit === "function" && (!("dispose" in plugin) || plugin.dispose === void 0 || typeof plugin.dispose === "function");
|
|
5110
5370
|
}
|
|
5111
5371
|
//#endregion
|
|
5112
5372
|
//#region ../../packages/server/dist/storageAccess.mjs
|
|
@@ -5209,34 +5469,48 @@ const hotUpdaterCoreMetadata = Symbol.for("@hot-updater/server/core-metadata");
|
|
|
5209
5469
|
function createHotUpdaterCore(options) {
|
|
5210
5470
|
for (const key of ["authorityId", "catalogId"]) if (Object.hasOwn(options, key)) throw new TypeError(`Remove ${key} from createHotUpdater options. Catalog identity is managed internally.`);
|
|
5211
5471
|
const database = options.database;
|
|
5212
|
-
const
|
|
5472
|
+
const storagePlugins = (options.storage ?? []).map((storage) => {
|
|
5213
5473
|
assertStorageOperations(storage, ["get", "getDownloadUrl"]);
|
|
5214
5474
|
return storage;
|
|
5215
|
-
})
|
|
5475
|
+
});
|
|
5476
|
+
const { downloadStorageObject, readStorageText, resolveFileUrl } = createStorageAccess(storagePlugins);
|
|
5216
5477
|
const adapterCapabilities = database;
|
|
5217
5478
|
if (!isDatabasePlugin(database)) throw new Error("@hot-updater/server only supports database plugins.");
|
|
5218
5479
|
const plugin = database;
|
|
5219
|
-
const
|
|
5480
|
+
const adapterName = adapterCapabilities.adapterName ?? plugin.name;
|
|
5481
|
+
const assertSchemaReady = createSchemaReadinessChecker(adapterName, adapterCapabilities.createMigrator);
|
|
5220
5482
|
const core = createDatabasePluginCore(plugin, resolveFileUrl, {
|
|
5221
5483
|
beforeOperation: assertSchemaReady,
|
|
5222
5484
|
readStorageText
|
|
5223
5485
|
});
|
|
5224
5486
|
const clientAccess = normalizeClientAccess(options.clientAccess);
|
|
5225
|
-
const
|
|
5226
|
-
async
|
|
5487
|
+
const insights = createInsightsProvider({
|
|
5488
|
+
async record(input) {
|
|
5489
|
+
await assertSchemaReady();
|
|
5490
|
+
return plugin.models.insights.record(input);
|
|
5491
|
+
},
|
|
5492
|
+
async listEvents(input) {
|
|
5493
|
+
await assertSchemaReady();
|
|
5494
|
+
return plugin.models.insights.listEvents(input);
|
|
5495
|
+
},
|
|
5496
|
+
async findInstallations(input) {
|
|
5497
|
+
await assertSchemaReady();
|
|
5498
|
+
return plugin.models.insights.findInstallations(input);
|
|
5499
|
+
},
|
|
5500
|
+
async countInstallations(input) {
|
|
5227
5501
|
await assertSchemaReady();
|
|
5228
|
-
return plugin.models.
|
|
5502
|
+
return plugin.models.insights.countInstallations(input);
|
|
5229
5503
|
},
|
|
5230
|
-
async
|
|
5504
|
+
async countEvents(input) {
|
|
5231
5505
|
await assertSchemaReady();
|
|
5232
|
-
return plugin.models.
|
|
5506
|
+
return plugin.models.insights.countEvents(input);
|
|
5233
5507
|
}
|
|
5234
5508
|
});
|
|
5235
5509
|
const apiKeys = createApiKeyManagement({
|
|
5236
5510
|
apiKeys: plugin.models.apiKeys,
|
|
5237
5511
|
beforeOperation: assertSchemaReady
|
|
5238
5512
|
});
|
|
5239
|
-
const handlers = createHotUpdaterHandlers(core.api,
|
|
5513
|
+
const handlers = createHotUpdaterHandlers(core.api, insights, clientAccess.type === "api-key" ? {
|
|
5240
5514
|
authenticate: (request) => authenticateApiKey({
|
|
5241
5515
|
apiKeys: plugin.models.apiKeys,
|
|
5242
5516
|
beforeLookup: assertSchemaReady,
|
|
@@ -5247,7 +5521,7 @@ function createHotUpdaterCore(options) {
|
|
|
5247
5521
|
} : void 0, downloadStorageObject);
|
|
5248
5522
|
const api = Object.assign({
|
|
5249
5523
|
adapterName: adapterCapabilities.adapterName ?? core.adapterName,
|
|
5250
|
-
|
|
5524
|
+
insights,
|
|
5251
5525
|
apiKeys,
|
|
5252
5526
|
handlers
|
|
5253
5527
|
}, core.api);
|
|
@@ -5270,6 +5544,7 @@ function createHotUpdater(options) {
|
|
|
5270
5544
|
//#endregion
|
|
5271
5545
|
//#region src/firebaseDatabaseParserShared.ts
|
|
5272
5546
|
var FirebaseDatabaseDataError = class extends Error {
|
|
5547
|
+
source;
|
|
5273
5548
|
name = "FirebaseDatabaseDataError";
|
|
5274
5549
|
constructor(source) {
|
|
5275
5550
|
super(`Invalid Firebase database data at "${source}".`);
|
|
@@ -5381,6 +5656,24 @@ const parseFirebaseBundleEventRow = (value, source) => {
|
|
|
5381
5656
|
received_at_ms: number(property(input, "received_at_ms"), source)
|
|
5382
5657
|
};
|
|
5383
5658
|
};
|
|
5659
|
+
const parseFirebaseInsightsInstallationRow = (value, source) => {
|
|
5660
|
+
const input = record(value, source);
|
|
5661
|
+
const type = string(property(input, "type"), source);
|
|
5662
|
+
if (type !== "UPDATE_APPLIED" && type !== "RECOVERED" && type !== "RELEASE_ADOPTED" && type !== "UNCHANGED") throw new FirebaseDatabaseDataError(source);
|
|
5663
|
+
return {
|
|
5664
|
+
id: string(property(input, "id"), source),
|
|
5665
|
+
type,
|
|
5666
|
+
install_id: string(property(input, "install_id"), source),
|
|
5667
|
+
user_id: nullableString(property(input, "user_id"), source),
|
|
5668
|
+
username: nullableString(property(input, "username"), source),
|
|
5669
|
+
to_bundle_id: string(property(input, "to_bundle_id"), source),
|
|
5670
|
+
platform: platform(property(input, "platform"), source),
|
|
5671
|
+
app_version: string(property(input, "app_version"), source),
|
|
5672
|
+
channel: string(property(input, "channel"), source),
|
|
5673
|
+
cohort: string(property(input, "cohort"), source),
|
|
5674
|
+
received_at_ms: number(property(input, "received_at_ms"), source)
|
|
5675
|
+
};
|
|
5676
|
+
};
|
|
5384
5677
|
const parseFirebaseApiKeyRow = (value, source) => {
|
|
5385
5678
|
const input = record(value, source);
|
|
5386
5679
|
const role = string(property(input, "role"), source);
|
|
@@ -5461,13 +5754,15 @@ const matchesCondition = (row, condition) => {
|
|
|
5461
5754
|
switch (condition.operator ?? "eq") {
|
|
5462
5755
|
case "eq": {
|
|
5463
5756
|
if (typeof expected !== "string") return actual === expected;
|
|
5464
|
-
const
|
|
5757
|
+
const mode = "mode" in condition ? condition.mode : void 0;
|
|
5758
|
+
const comparison = normalizeStringComparison(actual, expected, mode);
|
|
5465
5759
|
return comparison !== null && comparison[0] === comparison[1];
|
|
5466
5760
|
}
|
|
5467
5761
|
case "ne": {
|
|
5468
5762
|
if (actual === null || actual === void 0) return false;
|
|
5469
5763
|
if (typeof expected !== "string") return actual !== expected;
|
|
5470
|
-
const
|
|
5764
|
+
const mode = "mode" in condition ? condition.mode : void 0;
|
|
5765
|
+
const comparison = normalizeStringComparison(actual, expected, mode);
|
|
5471
5766
|
return comparison === null || comparison[0] !== comparison[1];
|
|
5472
5767
|
}
|
|
5473
5768
|
case "gt":
|
|
@@ -5492,17 +5787,20 @@ const matchesCondition = (row, condition) => {
|
|
|
5492
5787
|
}
|
|
5493
5788
|
case "contains": {
|
|
5494
5789
|
if (typeof expected !== "string") return false;
|
|
5495
|
-
const
|
|
5790
|
+
const mode = "mode" in condition ? condition.mode : void 0;
|
|
5791
|
+
const comparison = normalizeStringComparison(actual, expected, mode);
|
|
5496
5792
|
return comparison?.[0].includes(comparison[1]) ?? false;
|
|
5497
5793
|
}
|
|
5498
5794
|
case "starts_with": {
|
|
5499
5795
|
if (typeof expected !== "string") return false;
|
|
5500
|
-
const
|
|
5796
|
+
const mode = "mode" in condition ? condition.mode : void 0;
|
|
5797
|
+
const comparison = normalizeStringComparison(actual, expected, mode);
|
|
5501
5798
|
return comparison?.[0].startsWith(comparison[1]) ?? false;
|
|
5502
5799
|
}
|
|
5503
5800
|
case "ends_with": {
|
|
5504
5801
|
if (typeof expected !== "string") return false;
|
|
5505
|
-
const
|
|
5802
|
+
const mode = "mode" in condition ? condition.mode : void 0;
|
|
5803
|
+
const comparison = normalizeStringComparison(actual, expected, mode);
|
|
5506
5804
|
return comparison?.[0].endsWith(comparison[1]) ?? false;
|
|
5507
5805
|
}
|
|
5508
5806
|
}
|
|
@@ -5547,6 +5845,7 @@ const queryFirebaseDatabaseRows = (rows, input) => {
|
|
|
5547
5845
|
//#endregion
|
|
5548
5846
|
//#region src/firebaseDatabaseState.ts
|
|
5549
5847
|
var FirebaseDatabaseConstraintError = class extends Error {
|
|
5848
|
+
constraint;
|
|
5550
5849
|
name = "FirebaseDatabaseConstraintError";
|
|
5551
5850
|
constructor(constraint) {
|
|
5552
5851
|
super(`Firebase database constraint failed: ${constraint}`);
|
|
@@ -5557,6 +5856,7 @@ const cloneFirebaseDatabaseSnapshot = (snapshot) => ({
|
|
|
5557
5856
|
bundles: new Map(snapshot.bundles),
|
|
5558
5857
|
bundlePatches: new Map(snapshot.bundlePatches),
|
|
5559
5858
|
bundleEvents: new Map(snapshot.bundleEvents),
|
|
5859
|
+
bundleInstallations: new Map(snapshot.bundleInstallations),
|
|
5560
5860
|
channels: new Map(snapshot.channels),
|
|
5561
5861
|
apiKeys: new Map(snapshot.apiKeys),
|
|
5562
5862
|
releaseCatalogs: new Map(snapshot.releaseCatalogs),
|
|
@@ -5586,6 +5886,13 @@ const createFirebaseDatabaseState = (snapshot) => ({
|
|
|
5586
5886
|
requireUnique(snapshot.bundleEvents, input.data.id, input.model);
|
|
5587
5887
|
snapshot.bundleEvents.set(input.data.id, input.data);
|
|
5588
5888
|
return input.data;
|
|
5889
|
+
case "bundle_installations": {
|
|
5890
|
+
const current = snapshot.bundleInstallations.get(input.data.install_id);
|
|
5891
|
+
if (current && input.onConflict === "ignore") return current;
|
|
5892
|
+
if (current) throw new FirebaseDatabaseConstraintError("bundle_installations.install_id.unique");
|
|
5893
|
+
snapshot.bundleInstallations.set(input.data.install_id, input.data);
|
|
5894
|
+
return input.data;
|
|
5895
|
+
}
|
|
5589
5896
|
case "releases":
|
|
5590
5897
|
requireUnique(snapshot.releases, input.data.id, input.model);
|
|
5591
5898
|
if (!snapshot.channels.has(input.data.channel_id)) throw new FirebaseDatabaseConstraintError("releases.channel_id.foreign-key");
|
|
@@ -5615,6 +5922,16 @@ const createFirebaseDatabaseState = (snapshot) => ({
|
|
|
5615
5922
|
}
|
|
5616
5923
|
},
|
|
5617
5924
|
async update(input) {
|
|
5925
|
+
if (input.model === "bundle_installations") {
|
|
5926
|
+
const current = [...snapshot.bundleInstallations.values()].find((row) => matchesFirebaseDatabaseWhere(row, input.where));
|
|
5927
|
+
if (!current) return null;
|
|
5928
|
+
const updated = {
|
|
5929
|
+
...current,
|
|
5930
|
+
...input.update
|
|
5931
|
+
};
|
|
5932
|
+
snapshot.bundleInstallations.set(current.install_id, updated);
|
|
5933
|
+
return updated;
|
|
5934
|
+
}
|
|
5618
5935
|
if (input.model === "api_keys") {
|
|
5619
5936
|
const current = [...snapshot.apiKeys.values()].find((row) => matchesFirebaseDatabaseWhere(row, input.where));
|
|
5620
5937
|
if (!current) return null;
|
|
@@ -5676,6 +5993,8 @@ const createFirebaseDatabaseState = (snapshot) => ({
|
|
|
5676
5993
|
case "bundles": return distinctCount([...snapshot.bundles.values()].filter((row) => matchesFirebaseDatabaseWhere(row, input.where)), input.distinct);
|
|
5677
5994
|
case "bundle_patches": return distinctCount([...snapshot.bundlePatches.values()].filter((row) => matchesFirebaseDatabaseWhere(row, input.where)), input.distinct);
|
|
5678
5995
|
case "releases": return distinctCount([...snapshot.releases.values()].filter((row) => matchesFirebaseDatabaseWhere(row, input.where)), input.distinct);
|
|
5996
|
+
case "bundle_installations": return distinctCount([...snapshot.bundleInstallations.values()].filter((row) => matchesFirebaseDatabaseWhere(row, input.where)), input.distinct);
|
|
5997
|
+
case "bundle_events": return distinctCount([...snapshot.bundleEvents.values()].filter((row) => matchesFirebaseDatabaseWhere(row, input.where)), input.distinct);
|
|
5679
5998
|
}
|
|
5680
5999
|
},
|
|
5681
6000
|
async findOne(input) {
|
|
@@ -5686,6 +6005,7 @@ const createFirebaseDatabaseState = (snapshot) => ({
|
|
|
5686
6005
|
case "bundle_patches": return [...snapshot.bundlePatches.values()].find((row) => matchesFirebaseDatabaseWhere(row, input.where)) ?? null;
|
|
5687
6006
|
case "releases": return [...snapshot.releases.values()].find((row) => matchesFirebaseDatabaseWhere(row, input.where)) ?? null;
|
|
5688
6007
|
case "release_catalogs": return [...snapshot.releaseCatalogs.values()].find((row) => matchesFirebaseDatabaseWhere(row, input.where)) ?? null;
|
|
6008
|
+
case "bundle_installations": return [...snapshot.bundleInstallations.values()].find((row) => matchesFirebaseDatabaseWhere(row, input.where)) ?? null;
|
|
5689
6009
|
}
|
|
5690
6010
|
},
|
|
5691
6011
|
async findMany(input) {
|
|
@@ -5693,6 +6013,7 @@ const createFirebaseDatabaseState = (snapshot) => ({
|
|
|
5693
6013
|
case "bundles": return queryFirebaseDatabaseRows([...snapshot.bundles.values()], input);
|
|
5694
6014
|
case "bundle_patches": return queryFirebaseDatabaseRows([...snapshot.bundlePatches.values()], input);
|
|
5695
6015
|
case "bundle_events": return queryFirebaseDatabaseRows([...snapshot.bundleEvents.values()], input);
|
|
6016
|
+
case "bundle_installations": return queryFirebaseDatabaseRows([...snapshot.bundleInstallations.values()], input);
|
|
5696
6017
|
case "channels": return queryFirebaseDatabaseRows([...snapshot.channels.values()], input);
|
|
5697
6018
|
case "api_keys": return queryFirebaseDatabaseRows([...snapshot.apiKeys.values()], input);
|
|
5698
6019
|
case "releases": return queryFirebaseDatabaseRows([...snapshot.releases.values()], input);
|
|
@@ -5705,6 +6026,7 @@ const createFirebaseDatabaseState = (snapshot) => ({
|
|
|
5705
6026
|
const FIREBASE_V1_COLLECTION_NAMES = {
|
|
5706
6027
|
apiKeys: "hot_updater_v1_api_keys",
|
|
5707
6028
|
bundleEvents: "hot_updater_v1_bundle_events",
|
|
6029
|
+
bundleInstallations: "hot_updater_v1_bundle_installations",
|
|
5708
6030
|
bundlePatches: "hot_updater_v1_bundle_patches",
|
|
5709
6031
|
bundles: "hot_updater_v1_bundles",
|
|
5710
6032
|
channels: "hot_updater_v1_channels",
|
|
@@ -5715,6 +6037,7 @@ const FIREBASE_V1_COLLECTION_NAMES = {
|
|
|
5715
6037
|
//#endregion
|
|
5716
6038
|
//#region src/firebaseDatabasePersistence.ts
|
|
5717
6039
|
var FirebaseDatabaseAdapterVersionError = class extends Error {
|
|
6040
|
+
version;
|
|
5718
6041
|
name = "FirebaseDatabaseAdapterVersionError";
|
|
5719
6042
|
constructor(version) {
|
|
5720
6043
|
super(`Unsupported Firebase database adapter version: ${String(version)}`);
|
|
@@ -5725,6 +6048,7 @@ const createFirebaseDatabaseCollections = (db) => ({
|
|
|
5725
6048
|
bundles: db.collection(FIREBASE_V1_COLLECTION_NAMES.bundles),
|
|
5726
6049
|
bundlePatches: db.collection(FIREBASE_V1_COLLECTION_NAMES.bundlePatches),
|
|
5727
6050
|
bundleEvents: db.collection(FIREBASE_V1_COLLECTION_NAMES.bundleEvents),
|
|
6051
|
+
bundleInstallations: db.collection(FIREBASE_V1_COLLECTION_NAMES.bundleInstallations),
|
|
5728
6052
|
channels: db.collection(FIREBASE_V1_COLLECTION_NAMES.channels),
|
|
5729
6053
|
apiKeys: db.collection(FIREBASE_V1_COLLECTION_NAMES.apiKeys),
|
|
5730
6054
|
releaseCatalogs: db.collection(FIREBASE_V1_COLLECTION_NAMES.releaseCatalogs),
|
|
@@ -5733,14 +6057,15 @@ const createFirebaseDatabaseCollections = (db) => ({
|
|
|
5733
6057
|
});
|
|
5734
6058
|
const firebaseChannelDocumentId = (name) => `name_${Buffer.from(name, "utf8").toString("base64url")}`;
|
|
5735
6059
|
const firebaseChannelIdDocumentId = (id) => `channel_id_${Buffer.from(id, "utf8").toString("base64url")}`;
|
|
6060
|
+
const firebaseInstallationDocumentId = (id) => `install_${Buffer.from(id, "utf8").toString("base64url")}`;
|
|
5736
6061
|
const requireFirebaseDocumentKey = (model, documentId, row) => {
|
|
5737
|
-
if (documentId !== ("id" in row ? row.id : row.scope_key)) throw new FirebaseDatabaseConstraintError(`${model}.id.document-key`);
|
|
6062
|
+
if (documentId !== (model === "bundle_installations" ? firebaseInstallationDocumentId(row.install_id) : "id" in row ? row.id : row.scope_key)) throw new FirebaseDatabaseConstraintError(`${model}.id.document-key`);
|
|
5738
6063
|
return row;
|
|
5739
6064
|
};
|
|
5740
6065
|
const documentMap = (model, documents) => {
|
|
5741
6066
|
const rows = /* @__PURE__ */ new Map();
|
|
5742
6067
|
for (const { row } of documents) {
|
|
5743
|
-
const key = "id" in row ? row.id : row.scope_key;
|
|
6068
|
+
const key = model === "bundle_installations" ? row.install_id : "id" in row ? row.id : row.scope_key;
|
|
5744
6069
|
if (rows.has(key)) throw new FirebaseDatabaseConstraintError(`${model}.id.unique`);
|
|
5745
6070
|
rows.set(key, row);
|
|
5746
6071
|
}
|
|
@@ -5755,10 +6080,6 @@ const patchMap = (snapshot) => documentMap("bundle_patches", snapshot.docs.map((
|
|
|
5755
6080
|
document,
|
|
5756
6081
|
row: parseFirebasePatchRow(document.data(), `bundle_patches/${document.id}`)
|
|
5757
6082
|
})));
|
|
5758
|
-
const eventMap = (snapshot) => documentMap("bundle_events", snapshot.docs.map((document) => ({
|
|
5759
|
-
document,
|
|
5760
|
-
row: parseFirebaseBundleEventRow(document.data(), `bundle_events/${document.id}`)
|
|
5761
|
-
})));
|
|
5762
6083
|
const channelMap = (snapshot) => {
|
|
5763
6084
|
const rows = /* @__PURE__ */ new Map();
|
|
5764
6085
|
const names = /* @__PURE__ */ new Set();
|
|
@@ -5789,18 +6110,18 @@ const toSnapshot = (documents) => {
|
|
|
5789
6110
|
return {
|
|
5790
6111
|
bundles: bundleMap(documents[0]),
|
|
5791
6112
|
bundlePatches: patchMap(documents[1]),
|
|
5792
|
-
bundleEvents:
|
|
5793
|
-
|
|
5794
|
-
|
|
5795
|
-
|
|
5796
|
-
|
|
6113
|
+
bundleEvents: /* @__PURE__ */ new Map(),
|
|
6114
|
+
bundleInstallations: /* @__PURE__ */ new Map(),
|
|
6115
|
+
channels: channelMap(documents[2]),
|
|
6116
|
+
apiKeys: apiKeyMap(documents[3]),
|
|
6117
|
+
releases: releaseMap(documents[4]),
|
|
6118
|
+
releaseCatalogs: releaseCatalogMap(documents[5])
|
|
5797
6119
|
};
|
|
5798
6120
|
};
|
|
5799
6121
|
const loadFirebaseDatabaseSnapshot = async (collections) => {
|
|
5800
|
-
const [bundles, patches,
|
|
6122
|
+
const [bundles, patches, channels, apiKeys, releases, releaseCatalogs] = await Promise.all([
|
|
5801
6123
|
collections.bundles.get(),
|
|
5802
6124
|
collections.bundlePatches.get(),
|
|
5803
|
-
collections.bundleEvents.get(),
|
|
5804
6125
|
collections.channels.get(),
|
|
5805
6126
|
collections.apiKeys.get(),
|
|
5806
6127
|
collections.releases.get(),
|
|
@@ -5809,7 +6130,6 @@ const loadFirebaseDatabaseSnapshot = async (collections) => {
|
|
|
5809
6130
|
return toSnapshot([
|
|
5810
6131
|
bundles,
|
|
5811
6132
|
patches,
|
|
5812
|
-
events,
|
|
5813
6133
|
channels,
|
|
5814
6134
|
apiKeys,
|
|
5815
6135
|
releases,
|
|
@@ -5817,10 +6137,9 @@ const loadFirebaseDatabaseSnapshot = async (collections) => {
|
|
|
5817
6137
|
]);
|
|
5818
6138
|
};
|
|
5819
6139
|
const loadFirebaseTransactionSnapshot = async (transaction, collections) => {
|
|
5820
|
-
const [bundles, patches,
|
|
6140
|
+
const [bundles, patches, channels, apiKeys, releases, releaseCatalogs] = await Promise.all([
|
|
5821
6141
|
transaction.get(collections.bundles),
|
|
5822
6142
|
transaction.get(collections.bundlePatches),
|
|
5823
|
-
transaction.get(collections.bundleEvents),
|
|
5824
6143
|
transaction.get(collections.channels),
|
|
5825
6144
|
transaction.get(collections.apiKeys),
|
|
5826
6145
|
transaction.get(collections.releases),
|
|
@@ -5829,7 +6148,6 @@ const loadFirebaseTransactionSnapshot = async (transaction, collections) => {
|
|
|
5829
6148
|
return toSnapshot([
|
|
5830
6149
|
bundles,
|
|
5831
6150
|
patches,
|
|
5832
|
-
events,
|
|
5833
6151
|
channels,
|
|
5834
6152
|
apiKeys,
|
|
5835
6153
|
releases,
|
|
@@ -5855,13 +6173,6 @@ const persistFirebaseDatabaseSnapshot = ({ transaction, collections, before, aft
|
|
|
5855
6173
|
after: after.bundlePatches,
|
|
5856
6174
|
documentId: (row) => row.id
|
|
5857
6175
|
});
|
|
5858
|
-
persistCollection({
|
|
5859
|
-
transaction,
|
|
5860
|
-
collection: collections.bundleEvents,
|
|
5861
|
-
before: before.bundleEvents,
|
|
5862
|
-
after: after.bundleEvents,
|
|
5863
|
-
documentId: (row) => row.id
|
|
5864
|
-
});
|
|
5865
6176
|
persistCollection({
|
|
5866
6177
|
transaction,
|
|
5867
6178
|
collection: collections.channels,
|
|
@@ -5893,7 +6204,7 @@ const persistFirebaseDatabaseSnapshot = ({ transaction, collections, before, aft
|
|
|
5893
6204
|
documentId: (row) => row.scope_key
|
|
5894
6205
|
});
|
|
5895
6206
|
};
|
|
5896
|
-
const migrateFirebaseDatabase = async (
|
|
6207
|
+
const migrateFirebaseDatabase = async (collections) => {
|
|
5897
6208
|
const versionDocument = collections.settings.doc("database_adapter_version");
|
|
5898
6209
|
const version = await versionDocument.get();
|
|
5899
6210
|
const adapterVersion = version.data()?.version;
|
|
@@ -5904,7 +6215,9 @@ const migrateFirebaseDatabase = async (_db, collections) => {
|
|
|
5904
6215
|
collections.bundlePatches.limit(1).get(),
|
|
5905
6216
|
collections.channels.limit(1).get(),
|
|
5906
6217
|
collections.releases.limit(1).get(),
|
|
5907
|
-
collections.releaseCatalogs.limit(1).get()
|
|
6218
|
+
collections.releaseCatalogs.limit(1).get(),
|
|
6219
|
+
collections.bundleInstallations.limit(1).get(),
|
|
6220
|
+
collections.bundleEvents.limit(1).get()
|
|
5908
6221
|
])).some((snapshot) => !snapshot.empty)) throw new FirebaseDatabaseAdapterVersionError("v0");
|
|
5909
6222
|
try {
|
|
5910
6223
|
await versionDocument.create({ version: 4 });
|
|
@@ -5919,13 +6232,40 @@ const exactId = (input) => {
|
|
|
5919
6232
|
const [condition] = input.where;
|
|
5920
6233
|
return condition.field === "id" && (condition.operator === void 0 || condition.operator === "eq") && typeof condition.value === "string" ? condition.value : void 0;
|
|
5921
6234
|
};
|
|
6235
|
+
const exactInstallId = (input) => {
|
|
6236
|
+
const condition = input.where?.find(({ field, operator }) => field === "install_id" && (operator === void 0 || operator === "eq"));
|
|
6237
|
+
return typeof condition?.value === "string" ? condition.value : void 0;
|
|
6238
|
+
};
|
|
6239
|
+
const firestoreOperator = (operator) => {
|
|
6240
|
+
switch (operator ?? "eq") {
|
|
6241
|
+
case "eq": return "==";
|
|
6242
|
+
case "ne": return "!=";
|
|
6243
|
+
case "gt": return ">";
|
|
6244
|
+
case "gte": return ">=";
|
|
6245
|
+
case "lt": return "<";
|
|
6246
|
+
case "lte": return "<=";
|
|
6247
|
+
case "in": return "in";
|
|
6248
|
+
case "not_in": return "not-in";
|
|
6249
|
+
default: return;
|
|
6250
|
+
}
|
|
6251
|
+
};
|
|
6252
|
+
const applyFirebaseWhere = (initial, where) => {
|
|
6253
|
+
let query = initial;
|
|
6254
|
+
for (const condition of where) {
|
|
6255
|
+
const operator = firestoreOperator(condition.operator);
|
|
6256
|
+
if (condition.connector === "OR" || operator === void 0) throw new FirebaseDatabaseConstraintError("query.unsupported");
|
|
6257
|
+
query = query.where(condition.field, operator, condition.value);
|
|
6258
|
+
}
|
|
6259
|
+
return query;
|
|
6260
|
+
};
|
|
5922
6261
|
const firebaseDatabase = (config) => {
|
|
5923
|
-
const
|
|
5924
|
-
const
|
|
6262
|
+
const implementation = (() => {
|
|
6263
|
+
const app = (0, firebase_admin_app.getApps)().length ? (0, firebase_admin_app.getApp)() : (0, firebase_admin_app.initializeApp)(config);
|
|
6264
|
+
const db = (0, firebase_admin_firestore.getFirestore)(app);
|
|
5925
6265
|
const collections = createFirebaseDatabaseCollections(db);
|
|
5926
6266
|
let migration;
|
|
5927
6267
|
const ensureMigrated = () => {
|
|
5928
|
-
migration ??= migrateFirebaseDatabase(
|
|
6268
|
+
migration ??= migrateFirebaseDatabase(collections).catch((error) => {
|
|
5929
6269
|
migration = void 0;
|
|
5930
6270
|
throw error;
|
|
5931
6271
|
});
|
|
@@ -5948,14 +6288,74 @@ const firebaseDatabase = (config) => {
|
|
|
5948
6288
|
};
|
|
5949
6289
|
const read = async (operation) => {
|
|
5950
6290
|
await ensureMigrated();
|
|
5951
|
-
|
|
6291
|
+
const snapshot = await loadFirebaseDatabaseSnapshot(collections);
|
|
6292
|
+
return operation(createFirebaseDatabaseState(snapshot));
|
|
5952
6293
|
};
|
|
5953
6294
|
return {
|
|
5954
|
-
|
|
5955
|
-
|
|
6295
|
+
recordInsights: async ({ event, installation }) => {
|
|
6296
|
+
await ensureMigrated();
|
|
6297
|
+
await db.runTransaction(async (transaction) => {
|
|
6298
|
+
const eventReference = collections.bundleEvents.doc(event.id);
|
|
6299
|
+
const installationReference = collections.bundleInstallations.doc(firebaseInstallationDocumentId(installation.install_id));
|
|
6300
|
+
const [storedEvent, storedInstallation] = await transaction.getAll(eventReference, installationReference);
|
|
6301
|
+
if (storedEvent.exists) return;
|
|
6302
|
+
const current = storedInstallation.exists ? requireFirebaseDocumentKey("bundle_installations", storedInstallation.id, parseFirebaseInsightsInstallationRow(storedInstallation.data(), `bundle_installations/${storedInstallation.id}`)) : null;
|
|
6303
|
+
transaction.create(eventReference, event);
|
|
6304
|
+
if (current === null || installation.received_at_ms > current.received_at_ms || installation.received_at_ms === current.received_at_ms && compareInsightsText(installation.id, current.id) > 0) transaction.set(installationReference, installation);
|
|
6305
|
+
});
|
|
6306
|
+
},
|
|
6307
|
+
create: async (input) => {
|
|
6308
|
+
if (input.model !== "bundle_events" && input.model !== "bundle_installations") return mutate((database) => database.create(input));
|
|
6309
|
+
await ensureMigrated();
|
|
6310
|
+
return db.runTransaction(async (transaction) => {
|
|
6311
|
+
const collection = input.model === "bundle_events" ? collections.bundleEvents : collections.bundleInstallations;
|
|
6312
|
+
const documentId = input.model === "bundle_events" ? input.data.id : firebaseInstallationDocumentId(input.data.install_id);
|
|
6313
|
+
const reference = collection.doc(documentId);
|
|
6314
|
+
const document = await transaction.get(reference);
|
|
6315
|
+
if (document.exists) {
|
|
6316
|
+
const row = input.model === "bundle_events" ? requireFirebaseDocumentKey("bundle_events", document.id, parseFirebaseBundleEventRow(document.data(), `bundle_events/${document.id}`)) : requireFirebaseDocumentKey("bundle_installations", document.id, parseFirebaseInsightsInstallationRow(document.data(), `bundle_installations/${document.id}`));
|
|
6317
|
+
if (input.onConflict === "ignore") return row;
|
|
6318
|
+
throw new FirebaseDatabaseConstraintError(`${input.model}.id.unique`);
|
|
6319
|
+
}
|
|
6320
|
+
transaction.create(reference, input.data);
|
|
6321
|
+
return input.data;
|
|
6322
|
+
});
|
|
6323
|
+
},
|
|
6324
|
+
update: async (input) => {
|
|
6325
|
+
if (input.model !== "bundle_installations") return mutate((database) => database.update(input));
|
|
6326
|
+
const installId = exactInstallId(input);
|
|
6327
|
+
if (installId === void 0) return mutate((database) => database.update(input));
|
|
6328
|
+
await ensureMigrated();
|
|
6329
|
+
return db.runTransaction(async (transaction) => {
|
|
6330
|
+
const reference = collections.bundleInstallations.doc(firebaseInstallationDocumentId(installId));
|
|
6331
|
+
const document = await transaction.get(reference);
|
|
6332
|
+
if (!document.exists) return null;
|
|
6333
|
+
const current = requireFirebaseDocumentKey("bundle_installations", document.id, parseFirebaseInsightsInstallationRow(document.data(), `bundle_installations/${document.id}`));
|
|
6334
|
+
if (!matchesFirebaseDatabaseWhere(current, input.where)) return null;
|
|
6335
|
+
const updated = {
|
|
6336
|
+
...current,
|
|
6337
|
+
...input.update
|
|
6338
|
+
};
|
|
6339
|
+
transaction.set(reference, updated, { merge: true });
|
|
6340
|
+
return updated;
|
|
6341
|
+
});
|
|
6342
|
+
},
|
|
5956
6343
|
delete: (input) => mutate((database) => database.delete(input)),
|
|
5957
|
-
count: (input) =>
|
|
6344
|
+
count: async (input) => {
|
|
6345
|
+
if (input.model !== "bundle_installations" && input.model !== "bundle_events") return read((database) => database.count(input));
|
|
6346
|
+
await ensureMigrated();
|
|
6347
|
+
let query = applyFirebaseWhere(input.model === "bundle_events" ? collections.bundleEvents : collections.bundleInstallations, input.where ?? []);
|
|
6348
|
+
if (input.model === "bundle_events") query = query.orderBy("received_at_ms", "desc").orderBy("id", "desc");
|
|
6349
|
+
return (await query.count().get()).data().count;
|
|
6350
|
+
},
|
|
5958
6351
|
findOne: async (input) => {
|
|
6352
|
+
if (input.model === "bundle_installations") {
|
|
6353
|
+
const installId = exactInstallId(input);
|
|
6354
|
+
if (installId === void 0) return read((database) => database.findOne(input));
|
|
6355
|
+
await ensureMigrated();
|
|
6356
|
+
const document = await collections.bundleInstallations.doc(firebaseInstallationDocumentId(installId)).get();
|
|
6357
|
+
return document.exists ? requireFirebaseDocumentKey("bundle_installations", document.id, parseFirebaseInsightsInstallationRow(document.data(), `bundle_installations/${document.id}`)) : null;
|
|
6358
|
+
}
|
|
5959
6359
|
const id = exactId(input);
|
|
5960
6360
|
if (id === void 0) return read((database) => database.findOne(input));
|
|
5961
6361
|
await ensureMigrated();
|
|
@@ -5976,9 +6376,21 @@ const firebaseDatabase = (config) => {
|
|
|
5976
6376
|
}
|
|
5977
6377
|
},
|
|
5978
6378
|
findMany: async (input) => {
|
|
5979
|
-
if (input.model
|
|
5980
|
-
|
|
5981
|
-
|
|
6379
|
+
if (input.model === "bundle_events" || input.model === "bundle_installations") {
|
|
6380
|
+
await ensureMigrated();
|
|
6381
|
+
const collection = input.model === "bundle_events" ? collections.bundleEvents : collections.bundleInstallations;
|
|
6382
|
+
let query = applyFirebaseWhere(collection, input.where ?? []);
|
|
6383
|
+
for (const order of input.orderBy ?? []) {
|
|
6384
|
+
if (order.nulls !== void 0) throw new FirebaseDatabaseConstraintError("query.unsupported");
|
|
6385
|
+
query = query.orderBy(order.field, order.direction);
|
|
6386
|
+
}
|
|
6387
|
+
return (await query.offset(input.offset).limit(input.limit).get()).docs.map((document) => input.model === "bundle_events" ? requireFirebaseDocumentKey("bundle_events", document.id, parseFirebaseBundleEventRow(document.data(), `bundle_events/${document.id}`)) : requireFirebaseDocumentKey("bundle_installations", document.id, parseFirebaseInsightsInstallationRow(document.data(), `bundle_installations/${document.id}`)));
|
|
6388
|
+
}
|
|
6389
|
+
if (input.model === "channels") {
|
|
6390
|
+
await ensureMigrated();
|
|
6391
|
+
return queryFirebaseDatabaseRows(await loadFirebaseChannels(collections), input);
|
|
6392
|
+
}
|
|
6393
|
+
return read((database) => database.findMany(input));
|
|
5982
6394
|
},
|
|
5983
6395
|
insertChannel: async (input) => {
|
|
5984
6396
|
await ensureMigrated();
|
|
@@ -6030,7 +6442,8 @@ const firebaseDatabase = (config) => {
|
|
|
6030
6442
|
},
|
|
6031
6443
|
transaction: (callback) => mutate(callback)
|
|
6032
6444
|
};
|
|
6033
|
-
})()
|
|
6445
|
+
})();
|
|
6446
|
+
const adapter = createDatabasePluginAdapter("firebaseDatabase", implementation);
|
|
6034
6447
|
return createDatabasePlugin({
|
|
6035
6448
|
name: "firebaseDatabase",
|
|
6036
6449
|
models: adapter.models,
|
|
@@ -6040,7 +6453,8 @@ const firebaseDatabase = (config) => {
|
|
|
6040
6453
|
//#endregion
|
|
6041
6454
|
//#region src/firebaseStorage.ts
|
|
6042
6455
|
const firebaseStorage = (config) => {
|
|
6043
|
-
const
|
|
6456
|
+
const app = (0, firebase_admin_app.getApps)().length ? (0, firebase_admin_app.getApp)() : (0, firebase_admin_app.initializeApp)(config);
|
|
6457
|
+
const bucket = (0, firebase_admin_storage.getStorage)(app).bucket(config.storageBucket);
|
|
6044
6458
|
const getStorageKey = createStorageKeyBuilder(config.basePath);
|
|
6045
6459
|
const parseAndValidate = (storageUri) => {
|
|
6046
6460
|
const parsed = parseStorageUri(storageUri, "gs");
|