@hot-updater/firebase 1.0.0-rc.0 → 1.0.0-rc.14
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 +1163 -866
- package/dist/firebase/public/firestore.indexes.json +46 -33
- package/dist/firebase/public/functions/_package.json +2 -2
- package/dist/iac/index.cjs +318 -183
- package/dist/iac/index.d.cts +1 -5
- package/dist/iac/index.d.mts +1 -5
- package/dist/iac/index.mjs +314 -179
- package/dist/index.cjs +121 -49
- package/dist/index.d.cts +2 -4
- package/dist/index.d.mts +1 -3
- package/dist/index.mjs +124 -52
- package/package.json +9 -9
|
@@ -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.14";
|
|
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}`);
|
|
@@ -619,6 +382,7 @@ const isDatabaseJsonValue = (value) => {
|
|
|
619
382
|
};
|
|
620
383
|
const isDatabaseJsonObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && isDatabaseJsonValue(value);
|
|
621
384
|
const isDatabaseMetadataObject = (value) => isDatabaseJsonObject(value) && (!Object.hasOwn(value, "app_version") || typeof value["app_version"] === "string");
|
|
385
|
+
const isDatabaseBundleEventMetadata = (value) => isDatabaseJsonObject(value) && (value.username === null || typeof value.username === "string") && typeof value.cohort === "string" && (value.update_strategy === null || value.update_strategy === "fingerprint" || value.update_strategy === "appVersion") && (value.fingerprint_hash === null || typeof value.fingerprint_hash === "string") && (value.sdk_version === null || typeof value.sdk_version === "string");
|
|
622
386
|
//#endregion
|
|
623
387
|
//#region ../../packages/core/dist/index.mjs
|
|
624
388
|
const stripBundleArtifactMetadata = (metadata) => metadata;
|
|
@@ -731,7 +495,7 @@ function getRolledOutNumericCohorts(bundleId, rolloutCohortCount) {
|
|
|
731
495
|
return Array.from({ length: normalizedRolloutCount }, (_, position) => positiveMod(multiplier * position + offset, NUMERIC_COHORT_SIZE)).map((zeroBasedCohort) => zeroBasedCohort + 1).sort((left, right) => left - right);
|
|
732
496
|
}
|
|
733
497
|
const RELEASE_CATALOG_FALLBACK_POLICY = "BUILTIN_IF_ACTIVE_INELIGIBLE";
|
|
734
|
-
const MAX_COMPILED_CATALOG_BYTES =
|
|
498
|
+
const MAX_COMPILED_CATALOG_BYTES = 262144;
|
|
735
499
|
const BASE64URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
736
500
|
const MAX_CATALOG_SEGMENT_LENGTH = 255;
|
|
737
501
|
function assertCatalogSegment(value, name) {
|
|
@@ -838,7 +602,7 @@ const isUUIDv7 = (value) => typeof value === "string" && UUID_V7_PATTERN.test(va
|
|
|
838
602
|
//#endregion
|
|
839
603
|
//#region ../plugin-core/dist/uuidv7.mjs
|
|
840
604
|
function createUUIDv7FromTimestampHex(timestampHex) {
|
|
841
|
-
const randomBytes = new Uint8Array(10);
|
|
605
|
+
const randomBytes = /* @__PURE__ */ new Uint8Array(10);
|
|
842
606
|
crypto.getRandomValues(randomBytes);
|
|
843
607
|
const randomHex = Array.from(randomBytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
844
608
|
const randA = randomHex.slice(0, 3);
|
|
@@ -921,7 +685,6 @@ const databaseFields = {
|
|
|
921
685
|
"type",
|
|
922
686
|
"install_id",
|
|
923
687
|
"user_id",
|
|
924
|
-
"username",
|
|
925
688
|
"from_bundle_id",
|
|
926
689
|
"from_release_id",
|
|
927
690
|
"to_release_id",
|
|
@@ -929,10 +692,7 @@ const databaseFields = {
|
|
|
929
692
|
"platform",
|
|
930
693
|
"app_version",
|
|
931
694
|
"channel",
|
|
932
|
-
"
|
|
933
|
-
"update_strategy",
|
|
934
|
-
"fingerprint_hash",
|
|
935
|
-
"sdk_version",
|
|
695
|
+
"metadata",
|
|
936
696
|
"received_at_ms"
|
|
937
697
|
],
|
|
938
698
|
api_keys: [
|
|
@@ -947,7 +707,7 @@ const databaseFields = {
|
|
|
947
707
|
};
|
|
948
708
|
//#endregion
|
|
949
709
|
//#region ../plugin-core/dist/databasePluginCrudValidationFields.mjs
|
|
950
|
-
const isRecord$
|
|
710
|
+
const isRecord$3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
951
711
|
const isChannelText = (value) => {
|
|
952
712
|
if (typeof value !== "string" || value.length === 0) return false;
|
|
953
713
|
let codePointCount = 0;
|
|
@@ -957,6 +717,8 @@ const isChannelText = (value) => {
|
|
|
957
717
|
}
|
|
958
718
|
return true;
|
|
959
719
|
};
|
|
720
|
+
const isInsightsIdentityText = (value) => typeof value === "string" && value.length > 0 && value.length <= 255;
|
|
721
|
+
const isNullableInsightsIdentityText = (value) => value === null || isInsightsIdentityText(value);
|
|
960
722
|
const modelValidators = {
|
|
961
723
|
bundles: {
|
|
962
724
|
id: (value) => typeof value === "string",
|
|
@@ -1031,10 +793,9 @@ const modelValidators = {
|
|
|
1031
793
|
},
|
|
1032
794
|
bundle_events: {
|
|
1033
795
|
id: (value) => typeof value === "string",
|
|
1034
|
-
type: (value) => value === "
|
|
1035
|
-
install_id:
|
|
1036
|
-
user_id:
|
|
1037
|
-
username: (value) => value === null || typeof value === "string",
|
|
796
|
+
type: (value) => value === "UPDATE_DOWNLOADED" || value === "UPDATE_APPLIED" || value === "RECOVERED" || value === "UNCHANGED",
|
|
797
|
+
install_id: isInsightsIdentityText,
|
|
798
|
+
user_id: isNullableInsightsIdentityText,
|
|
1038
799
|
from_bundle_id: (value) => value === null || typeof value === "string",
|
|
1039
800
|
from_release_id: (value) => value === null || typeof value === "string",
|
|
1040
801
|
to_release_id: (value) => value === null || typeof value === "string",
|
|
@@ -1042,10 +803,7 @@ const modelValidators = {
|
|
|
1042
803
|
platform: (value) => value === "ios" || value === "android",
|
|
1043
804
|
app_version: (value) => typeof value === "string",
|
|
1044
805
|
channel: (value) => typeof value === "string",
|
|
1045
|
-
|
|
1046
|
-
update_strategy: (value) => value === null || value === "fingerprint" || value === "appVersion",
|
|
1047
|
-
fingerprint_hash: (value) => value === null || typeof value === "string",
|
|
1048
|
-
sdk_version: (value) => value === null || typeof value === "string",
|
|
806
|
+
metadata: isDatabaseBundleEventMetadata,
|
|
1049
807
|
received_at_ms: (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0
|
|
1050
808
|
},
|
|
1051
809
|
api_keys: {
|
|
@@ -1058,7 +816,7 @@ const modelValidators = {
|
|
|
1058
816
|
revoked_at_ms: (value) => value === null || typeof value === "number" && Number.isSafeInteger(value) && value >= 0
|
|
1059
817
|
}
|
|
1060
818
|
};
|
|
1061
|
-
const stringFields = new Set([
|
|
819
|
+
const stringFields = /* @__PURE__ */ new Set([
|
|
1062
820
|
"id",
|
|
1063
821
|
"platform",
|
|
1064
822
|
"file_hash",
|
|
@@ -1099,7 +857,7 @@ const stringFields = new Set([
|
|
|
1099
857
|
"prefix",
|
|
1100
858
|
"role"
|
|
1101
859
|
]);
|
|
1102
|
-
const numberFields = new Set([
|
|
860
|
+
const numberFields = /* @__PURE__ */ new Set([
|
|
1103
861
|
"archive_byte_size",
|
|
1104
862
|
"byte_size",
|
|
1105
863
|
"rollout_cohort_count",
|
|
@@ -1112,13 +870,13 @@ const numberFields = new Set([
|
|
|
1112
870
|
"created_at_ms",
|
|
1113
871
|
"revoked_at_ms"
|
|
1114
872
|
]);
|
|
1115
|
-
const booleanFields = new Set([
|
|
873
|
+
const booleanFields = /* @__PURE__ */ new Set([
|
|
1116
874
|
"should_force_update",
|
|
1117
875
|
"enabled",
|
|
1118
876
|
"is_tombstone"
|
|
1119
877
|
]);
|
|
1120
878
|
const sortableFields = {
|
|
1121
|
-
bundles: new Set([
|
|
879
|
+
bundles: /* @__PURE__ */ new Set([
|
|
1122
880
|
"id",
|
|
1123
881
|
"platform",
|
|
1124
882
|
"file_hash",
|
|
@@ -1135,7 +893,7 @@ const sortableFields = {
|
|
|
1135
893
|
"manifest_file_hash",
|
|
1136
894
|
"asset_base_storage_uri"
|
|
1137
895
|
]),
|
|
1138
|
-
bundle_patches: new Set([
|
|
896
|
+
bundle_patches: /* @__PURE__ */ new Set([
|
|
1139
897
|
"id",
|
|
1140
898
|
"bundle_id",
|
|
1141
899
|
"base_bundle_id",
|
|
@@ -1145,7 +903,7 @@ const sortableFields = {
|
|
|
1145
903
|
"byte_size",
|
|
1146
904
|
"order_index"
|
|
1147
905
|
]),
|
|
1148
|
-
releases: new Set([
|
|
906
|
+
releases: /* @__PURE__ */ new Set([
|
|
1149
907
|
"id",
|
|
1150
908
|
"revision",
|
|
1151
909
|
"scope_key",
|
|
@@ -1165,7 +923,7 @@ const sortableFields = {
|
|
|
1165
923
|
"created_at_ms",
|
|
1166
924
|
"updated_at_ms"
|
|
1167
925
|
]),
|
|
1168
|
-
release_catalogs: new Set([
|
|
926
|
+
release_catalogs: /* @__PURE__ */ new Set([
|
|
1169
927
|
"scope_key",
|
|
1170
928
|
"catalog_id",
|
|
1171
929
|
"strategy",
|
|
@@ -1179,8 +937,8 @@ const sortableFields = {
|
|
|
1179
937
|
"is_tombstone",
|
|
1180
938
|
"updated_at_ms"
|
|
1181
939
|
]),
|
|
1182
|
-
channels: new Set(["id", "name"]),
|
|
1183
|
-
bundle_events: new Set([
|
|
940
|
+
channels: /* @__PURE__ */ new Set(["id", "name"]),
|
|
941
|
+
bundle_events: /* @__PURE__ */ new Set([
|
|
1184
942
|
"id",
|
|
1185
943
|
"type",
|
|
1186
944
|
"install_id",
|
|
@@ -1190,6 +948,8 @@ const sortableFields = {
|
|
|
1190
948
|
"from_release_id",
|
|
1191
949
|
"to_release_id",
|
|
1192
950
|
"to_bundle_id",
|
|
951
|
+
"pending_bundle_id",
|
|
952
|
+
"pending_release_id",
|
|
1193
953
|
"platform",
|
|
1194
954
|
"app_version",
|
|
1195
955
|
"channel",
|
|
@@ -1199,7 +959,7 @@ const sortableFields = {
|
|
|
1199
959
|
"sdk_version",
|
|
1200
960
|
"received_at_ms"
|
|
1201
961
|
]),
|
|
1202
|
-
api_keys: new Set([
|
|
962
|
+
api_keys: /* @__PURE__ */ new Set([
|
|
1203
963
|
"id",
|
|
1204
964
|
"hash",
|
|
1205
965
|
"name",
|
|
@@ -1231,9 +991,9 @@ const hasValidReleaseInvariants = (data) => {
|
|
|
1231
991
|
const fingerprintHash = data.fingerprint_hash;
|
|
1232
992
|
return (kind === "BUNDLE" && typeof bundleId === "string" || kind === "EMBEDDED" && bundleId === null) && (strategy === "APP_VERSION" && typeof targetAppVersion === "string" && fingerprintHash === null || strategy === "FINGERPRINT" && targetAppVersion === null && typeof fingerprintHash === "string");
|
|
1233
993
|
};
|
|
1234
|
-
const hasValidBundleEventInvariants = (data) => (data.type === "
|
|
994
|
+
const hasValidBundleEventInvariants = (data) => (data.type === "UPDATE_DOWNLOADED" || data.type === "UPDATE_APPLIED" || data.type === "RECOVERED") && typeof data.from_bundle_id === "string" && ((isRecord$3(data.metadata) ? data.metadata.update_strategy : void 0) === "fingerprint" || (isRecord$3(data.metadata) ? data.metadata.update_strategy : void 0) === "appVersion") || data.type === "UNCHANGED" && data.from_bundle_id === null && (isRecord$3(data.metadata) ? data.metadata.update_strategy : void 0) === null;
|
|
1235
995
|
const validateCreateData = (model, data) => {
|
|
1236
|
-
if (!isRecord$
|
|
996
|
+
if (!isRecord$3(data)) throw new DatabasePluginInputError("invalid-data");
|
|
1237
997
|
validateFields(model, Object.keys(data));
|
|
1238
998
|
for (const field of databaseFields[model]) {
|
|
1239
999
|
const validator = modelValidators[model][field];
|
|
@@ -1250,7 +1010,7 @@ const selectRow = (row, input) => {
|
|
|
1250
1010
|
return Object.fromEntries(select.map((field) => [field, Reflect.get(row, field)]));
|
|
1251
1011
|
};
|
|
1252
1012
|
const validateResult = (model, row, select) => {
|
|
1253
|
-
if (!isRecord$
|
|
1013
|
+
if (!isRecord$3(row)) throw new DatabasePluginInputError("invalid-result");
|
|
1254
1014
|
const fields = select ?? databaseFields[model];
|
|
1255
1015
|
for (const field of fields) {
|
|
1256
1016
|
const validator = modelValidators[model][field];
|
|
@@ -1267,10 +1027,148 @@ const validateResult = (model, row, select) => {
|
|
|
1267
1027
|
if (model === "bundle_events" && [
|
|
1268
1028
|
"type",
|
|
1269
1029
|
"from_bundle_id",
|
|
1270
|
-
"
|
|
1030
|
+
"metadata"
|
|
1271
1031
|
].every((field) => Object.hasOwn(row, field)) && !hasValidBundleEventInvariants(row)) throw new DatabasePluginInputError("invalid-result");
|
|
1272
1032
|
};
|
|
1273
1033
|
//#endregion
|
|
1034
|
+
//#region ../plugin-core/dist/insightsContract.mjs
|
|
1035
|
+
const encoder = new TextEncoder();
|
|
1036
|
+
/** Exact UTF-8 byte ordering, without case folding or Unicode normalization. */
|
|
1037
|
+
const compareInsightsText = (left, right) => {
|
|
1038
|
+
const a = encoder.encode(left);
|
|
1039
|
+
const b = encoder.encode(right);
|
|
1040
|
+
for (let index = 0; index < Math.min(a.length, b.length); index += 1) if (a[index] !== b[index]) return a[index] - b[index];
|
|
1041
|
+
return a.length - b.length;
|
|
1042
|
+
};
|
|
1043
|
+
const isWellFormedText = (value) => {
|
|
1044
|
+
for (const character of value) {
|
|
1045
|
+
const point = character.codePointAt(0);
|
|
1046
|
+
if (point >= 55296 && point <= 57343) return false;
|
|
1047
|
+
}
|
|
1048
|
+
return true;
|
|
1049
|
+
};
|
|
1050
|
+
const isIdentity = (value) => typeof value === "string" && value.length > 0 && value.length <= 255 && isWellFormedText(value);
|
|
1051
|
+
const isText = (value) => typeof value === "string" && value.length > 0 && isWellFormedText(value);
|
|
1052
|
+
const isTimestamp = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
1053
|
+
const isLimit = (value) => isTimestamp(value) && value >= 1 && value <= 101;
|
|
1054
|
+
const hasOnlyKeys$1 = (value, keys) => Object.keys(value).every((key) => keys.includes(key));
|
|
1055
|
+
const hasScope = (value) => (value.platform === "ios" || value.platform === "android") && isText(value.channel);
|
|
1056
|
+
const isBundleFilter = (value, withKind = false) => {
|
|
1057
|
+
if (!isRecord$3(value) || !hasScope(value)) return false;
|
|
1058
|
+
const keys = [
|
|
1059
|
+
"platform",
|
|
1060
|
+
"channel",
|
|
1061
|
+
"type",
|
|
1062
|
+
...withKind ? ["kind"] : []
|
|
1063
|
+
];
|
|
1064
|
+
return value.type === "RECOVERED" ? isText(value.fromBundleId) && hasOnlyKeys$1(value, [...keys, "fromBundleId"]) : (value.type === "UPDATE_DOWNLOADED" || value.type === "UPDATE_APPLIED" || value.type === "UNCHANGED") && isText(value.toBundleId) && hasOnlyKeys$1(value, [...keys, "toBundleId"]);
|
|
1065
|
+
};
|
|
1066
|
+
const isEventFilter = (value) => {
|
|
1067
|
+
if (!isRecord$3(value)) return false;
|
|
1068
|
+
if (value.kind === "all") return hasOnlyKeys$1(value, ["kind"]);
|
|
1069
|
+
if (value.kind === "installationMovement") return isIdentity(value.installId) && hasOnlyKeys$1(value, ["kind", "installId"]);
|
|
1070
|
+
return value.kind === "bundle" && isBundleFilter(value, true);
|
|
1071
|
+
};
|
|
1072
|
+
const validateRow = (model, row, result = false) => {
|
|
1073
|
+
try {
|
|
1074
|
+
validateCreateData(model, row);
|
|
1075
|
+
if (!isRecord$3(row) || typeof row.id !== "string" || !isUUIDv7(row.id) || [...Object.values(row), ...isRecord$3(row.metadata) ? Object.values(row.metadata) : []].some((value) => typeof value === "string" && !isWellFormedText(value))) throw new DatabasePluginInputError("invalid-data");
|
|
1076
|
+
} catch (error) {
|
|
1077
|
+
if (result) throw new DatabasePluginInputError("invalid-result");
|
|
1078
|
+
throw error;
|
|
1079
|
+
}
|
|
1080
|
+
};
|
|
1081
|
+
/** Downloads and applied transitions belong in installation history. */
|
|
1082
|
+
const isInsightsMovementEvent = (event) => event.type === "UPDATE_DOWNLOADED" || event.type === "UPDATE_APPLIED" || event.type === "RECOVERED";
|
|
1083
|
+
const matchesInsightsEventFilter = (event, filter) => {
|
|
1084
|
+
if (filter.kind === "all") return true;
|
|
1085
|
+
if (filter.kind === "installationMovement") return event.install_id === filter.installId && isInsightsMovementEvent(event);
|
|
1086
|
+
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);
|
|
1087
|
+
};
|
|
1088
|
+
const invalidQuery = () => {
|
|
1089
|
+
throw new DatabasePluginInputError("invalid-query");
|
|
1090
|
+
};
|
|
1091
|
+
const invalidResult = () => {
|
|
1092
|
+
throw new DatabasePluginInputError("invalid-result");
|
|
1093
|
+
};
|
|
1094
|
+
const validateCount = (count) => isTimestamp(count) ? count : invalidResult();
|
|
1095
|
+
/** Validate custom and bundled providers at the same public boundary. */
|
|
1096
|
+
const createValidatedInsightsModel = (model) => ({
|
|
1097
|
+
async recordEvent(input) {
|
|
1098
|
+
if (!isRecord$3(input) || !hasOnlyKeys$1(input, ["event"])) throw new DatabasePluginInputError("invalid-data");
|
|
1099
|
+
validateRow("bundle_events", input.event);
|
|
1100
|
+
await model.recordEvent(input);
|
|
1101
|
+
},
|
|
1102
|
+
async listEvents(input) {
|
|
1103
|
+
if (!isRecord$3(input) || !hasOnlyKeys$1(input, [
|
|
1104
|
+
"filter",
|
|
1105
|
+
"sinceMs",
|
|
1106
|
+
"beforeReceivedAtMs",
|
|
1107
|
+
"after",
|
|
1108
|
+
"limit"
|
|
1109
|
+
]) || !isEventFilter(input.filter) || !isLimit(input.limit) || !isTimestamp(input.beforeReceivedAtMs) || input.sinceMs !== void 0 && !isTimestamp(input.sinceMs)) invalidQuery();
|
|
1110
|
+
const sinceMs = input.sinceMs ?? 0;
|
|
1111
|
+
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();
|
|
1112
|
+
const rows = await model.listEvents(input);
|
|
1113
|
+
if (!Array.isArray(rows) || rows.length > input.limit) invalidResult();
|
|
1114
|
+
let previous = input.after;
|
|
1115
|
+
for (const row of rows) {
|
|
1116
|
+
validateRow("bundle_events", row, true);
|
|
1117
|
+
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();
|
|
1118
|
+
previous = {
|
|
1119
|
+
receivedAtMs: row.received_at_ms,
|
|
1120
|
+
id: row.id
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
return rows;
|
|
1124
|
+
},
|
|
1125
|
+
async findLatestEvents(input) {
|
|
1126
|
+
if (!isRecord$3(input)) invalidQuery();
|
|
1127
|
+
if ("installId" in input) {
|
|
1128
|
+
if (!hasOnlyKeys$1(input, ["installId"]) || !isIdentity(input.installId)) invalidQuery();
|
|
1129
|
+
} else if (!hasOnlyKeys$1(input, [
|
|
1130
|
+
"userId",
|
|
1131
|
+
"afterInstallId",
|
|
1132
|
+
"limit"
|
|
1133
|
+
]) || !isIdentity(input.userId) || !isLimit(input.limit) || input.afterInstallId !== void 0 && !isIdentity(input.afterInstallId)) invalidQuery();
|
|
1134
|
+
const rows = await model.findLatestEvents(input);
|
|
1135
|
+
if (!Array.isArray(rows) || rows.length > ("installId" in input ? 1 : input.limit)) invalidResult();
|
|
1136
|
+
let previous = "installId" in input ? void 0 : input.afterInstallId;
|
|
1137
|
+
for (const row of rows) {
|
|
1138
|
+
validateRow("bundle_events", row, true);
|
|
1139
|
+
if ("installId" in input ? row.install_id !== input.installId : row.user_id !== input.userId || previous !== void 0 && compareInsightsText(row.install_id, previous) <= 0) invalidResult();
|
|
1140
|
+
previous = row.install_id;
|
|
1141
|
+
}
|
|
1142
|
+
return rows;
|
|
1143
|
+
},
|
|
1144
|
+
async countLatestEvents(input) {
|
|
1145
|
+
if (!isRecord$3(input) || !hasOnlyKeys$1(input, [
|
|
1146
|
+
"platform",
|
|
1147
|
+
"channel",
|
|
1148
|
+
"sinceMs",
|
|
1149
|
+
"bundle"
|
|
1150
|
+
]) || !hasScope(input) || !isTimestamp(input.sinceMs) || input.bundle !== void 0 && (!Array.isArray(input.bundle) || input.bundle.length < 1 || input.bundle.length > 2 || input.bundle.some((bundle) => !isRecord$3(bundle) || !hasOnlyKeys$1(bundle, [
|
|
1151
|
+
"field",
|
|
1152
|
+
"value",
|
|
1153
|
+
"types"
|
|
1154
|
+
]) || bundle.field !== "from_bundle_id" && bundle.field !== "to_bundle_id" || !isText(bundle.value) || !Array.isArray(bundle.types) || bundle.types.length === 0 || bundle.types.some((type) => ![
|
|
1155
|
+
"UNCHANGED",
|
|
1156
|
+
"UPDATE_DOWNLOADED",
|
|
1157
|
+
"UPDATE_APPLIED",
|
|
1158
|
+
"RECOVERED"
|
|
1159
|
+
].includes(type))))) invalidQuery();
|
|
1160
|
+
return validateCount(await model.countLatestEvents(input));
|
|
1161
|
+
},
|
|
1162
|
+
async countEvents(input) {
|
|
1163
|
+
if (!isRecord$3(input) || !hasOnlyKeys$1(input, [
|
|
1164
|
+
"filter",
|
|
1165
|
+
"sinceMs",
|
|
1166
|
+
"beforeReceivedAtMs"
|
|
1167
|
+
]) || !isBundleFilter(input.filter) || !isTimestamp(input.sinceMs) || !isTimestamp(input.beforeReceivedAtMs) || input.sinceMs > input.beforeReceivedAtMs) invalidQuery();
|
|
1168
|
+
return validateCount(await model.countEvents(input));
|
|
1169
|
+
}
|
|
1170
|
+
});
|
|
1171
|
+
//#endregion
|
|
1274
1172
|
//#region ../plugin-core/dist/databasePluginCrudValidationMutations.mjs
|
|
1275
1173
|
const validateMutationWhere = (where) => {
|
|
1276
1174
|
if (where.length === 0) throw new DatabasePluginInputError("empty-mutation-where");
|
|
@@ -1278,10 +1176,10 @@ const validateMutationWhere = (where) => {
|
|
|
1278
1176
|
const validateUpdateWhere = (model, where) => {
|
|
1279
1177
|
const selector = where[0];
|
|
1280
1178
|
const primaryField = model === "release_catalogs" ? "scope_key" : "id";
|
|
1281
|
-
if (where.length !== 1 || !isRecord$
|
|
1179
|
+
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");
|
|
1282
1180
|
};
|
|
1283
1181
|
const validateBundleUpdateData = (update) => {
|
|
1284
|
-
if (!isRecord$
|
|
1182
|
+
if (!isRecord$3(update)) throw new DatabasePluginInputError("invalid-data");
|
|
1285
1183
|
for (const [field, value] of Object.entries(update)) {
|
|
1286
1184
|
if (field === "id") throw new DatabasePluginInputError("invalid-data");
|
|
1287
1185
|
validateField("bundles", field);
|
|
@@ -1290,9 +1188,9 @@ const validateBundleUpdateData = (update) => {
|
|
|
1290
1188
|
}
|
|
1291
1189
|
};
|
|
1292
1190
|
const validateApiKeyUpdateData = (update) => {
|
|
1293
|
-
if (!isRecord$
|
|
1191
|
+
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
1192
|
};
|
|
1295
|
-
const RELEASE_MUTABLE_FIELDS = new Set([
|
|
1193
|
+
const RELEASE_MUTABLE_FIELDS = /* @__PURE__ */ new Set([
|
|
1296
1194
|
"revision",
|
|
1297
1195
|
"scope_key",
|
|
1298
1196
|
"target_app_version",
|
|
@@ -1305,7 +1203,7 @@ const RELEASE_MUTABLE_FIELDS = new Set([
|
|
|
1305
1203
|
"updated_at_ms"
|
|
1306
1204
|
]);
|
|
1307
1205
|
const validateReleaseUpdateData = (update) => {
|
|
1308
|
-
if (!isRecord$
|
|
1206
|
+
if (!isRecord$3(update) || Reflect.ownKeys(update).length === 0) throw new DatabasePluginInputError("invalid-data");
|
|
1309
1207
|
for (const [field, value] of Object.entries(update)) {
|
|
1310
1208
|
if (!RELEASE_MUTABLE_FIELDS.has(field)) throw new DatabasePluginInputError("invalid-data");
|
|
1311
1209
|
const validator = modelValidators.releases[field];
|
|
@@ -1313,7 +1211,7 @@ const validateReleaseUpdateData = (update) => {
|
|
|
1313
1211
|
}
|
|
1314
1212
|
};
|
|
1315
1213
|
const validateReleaseCatalogUpdateData = (update) => {
|
|
1316
|
-
if (!isRecord$
|
|
1214
|
+
if (!isRecord$3(update)) throw new DatabasePluginInputError("invalid-data");
|
|
1317
1215
|
const expectedFields = Object.keys(modelValidators.release_catalogs).filter((field) => field !== "scope_key");
|
|
1318
1216
|
if (Reflect.ownKeys(update).length !== expectedFields.length || expectedFields.some((field) => !Object.hasOwn(update, field))) throw new DatabasePluginInputError("invalid-data");
|
|
1319
1217
|
for (const [field, value] of Object.entries(update)) {
|
|
@@ -1393,7 +1291,7 @@ const validateWhere$1 = (model, where) => {
|
|
|
1393
1291
|
if (where === void 0) return;
|
|
1394
1292
|
if (!Array.isArray(where)) throw new DatabasePluginInputError("invalid-query");
|
|
1395
1293
|
for (const item of where) {
|
|
1396
|
-
if (!isRecord$
|
|
1294
|
+
if (!isRecord$3(item)) throw new DatabasePluginInputError("invalid-query");
|
|
1397
1295
|
if (item.connector !== void 0 && item.connector !== "AND" && item.connector !== "OR") throw new DatabasePluginInputError("invalid-query");
|
|
1398
1296
|
validateWhereValue(model, item);
|
|
1399
1297
|
}
|
|
@@ -1410,7 +1308,7 @@ const validateOrderBy = (model, orderBy) => {
|
|
|
1410
1308
|
if (!Array.isArray(orderBy) || orderBy.length === 0) throw new DatabasePluginInputError("invalid-query");
|
|
1411
1309
|
const fields = /* @__PURE__ */ new Set();
|
|
1412
1310
|
return orderBy.map((clause) => {
|
|
1413
|
-
if (!isRecord$
|
|
1311
|
+
if (!isRecord$3(clause) || typeof clause.field !== "string") throw new DatabasePluginInputError("invalid-query");
|
|
1414
1312
|
validateField(model, clause.field);
|
|
1415
1313
|
if (!sortableFields[model].has(clause.field)) throw new DatabasePluginInputError("invalid-query");
|
|
1416
1314
|
if (clause.direction !== "asc" && clause.direction !== "desc") throw new DatabasePluginInputError("invalid-query");
|
|
@@ -1422,7 +1320,7 @@ const validateOrderBy = (model, orderBy) => {
|
|
|
1422
1320
|
};
|
|
1423
1321
|
const validateDistinctOn = (model, distinctOn, orderBy) => {
|
|
1424
1322
|
if (distinctOn === void 0) return;
|
|
1425
|
-
if (!isRecord$
|
|
1323
|
+
if (!isRecord$3(distinctOn)) throw new DatabasePluginInputError("invalid-distinct");
|
|
1426
1324
|
const fields = validateDistinctFields(model, distinctOn.fields);
|
|
1427
1325
|
if (fields === void 0 || orderBy === void 0) throw new DatabasePluginInputError("invalid-distinct");
|
|
1428
1326
|
for (const [index, field] of fields.entries()) if (orderBy[index]?.field !== field) throw new DatabasePluginInputError("invalid-distinct");
|
|
@@ -1436,7 +1334,7 @@ const validateBundlePagination = (options) => {
|
|
|
1436
1334
|
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
1335
|
const cursor = options.cursor;
|
|
1438
1336
|
if (cursor === void 0) return;
|
|
1439
|
-
if (!isRecord$
|
|
1337
|
+
if (!isRecord$3(cursor)) throw new DatabasePluginInputError("invalid-pagination");
|
|
1440
1338
|
const hasAfter = Object.hasOwn(cursor, "after");
|
|
1441
1339
|
if (hasAfter === Object.hasOwn(cursor, "before")) throw new DatabasePluginInputError("invalid-pagination");
|
|
1442
1340
|
const value = hasAfter ? cursor.after : cursor.before;
|
|
@@ -1531,7 +1429,91 @@ const createTransactionDatabasePlugin = (implementation) => {
|
|
|
1531
1429
|
//#region ../plugin-core/dist/createDatabasePlugin.mjs
|
|
1532
1430
|
const PAGE_SIZE$1 = 100;
|
|
1533
1431
|
const compareChannelRows = (left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0;
|
|
1432
|
+
const toInsightsBundleWhere = (filter) => [
|
|
1433
|
+
{
|
|
1434
|
+
field: "platform",
|
|
1435
|
+
value: filter.platform
|
|
1436
|
+
},
|
|
1437
|
+
{
|
|
1438
|
+
field: "channel",
|
|
1439
|
+
value: filter.channel
|
|
1440
|
+
},
|
|
1441
|
+
{
|
|
1442
|
+
field: "type",
|
|
1443
|
+
value: filter.type
|
|
1444
|
+
},
|
|
1445
|
+
filter.type === "RECOVERED" ? {
|
|
1446
|
+
field: "from_bundle_id",
|
|
1447
|
+
value: filter.fromBundleId
|
|
1448
|
+
} : {
|
|
1449
|
+
field: "to_bundle_id",
|
|
1450
|
+
value: filter.toBundleId
|
|
1451
|
+
}
|
|
1452
|
+
];
|
|
1453
|
+
const toInsightsEventRanges = (filter) => {
|
|
1454
|
+
if (filter.kind === "all") return [[]];
|
|
1455
|
+
if (filter.kind === "bundle") return [toInsightsBundleWhere(filter)];
|
|
1456
|
+
return [
|
|
1457
|
+
"UPDATE_DOWNLOADED",
|
|
1458
|
+
"UPDATE_APPLIED",
|
|
1459
|
+
"RECOVERED"
|
|
1460
|
+
].map((type) => [{
|
|
1461
|
+
field: "install_id",
|
|
1462
|
+
value: filter.installId
|
|
1463
|
+
}, {
|
|
1464
|
+
field: "type",
|
|
1465
|
+
value: type
|
|
1466
|
+
}]);
|
|
1467
|
+
};
|
|
1468
|
+
const listInsightsEventRange = async (crud, input, filterWhere) => {
|
|
1469
|
+
const where = [...filterWhere, {
|
|
1470
|
+
field: "received_at_ms",
|
|
1471
|
+
operator: "gte",
|
|
1472
|
+
value: input.sinceMs ?? 0
|
|
1473
|
+
}];
|
|
1474
|
+
const sameTimestamp = input.after === void 0 ? [] : await crud.findMany({
|
|
1475
|
+
model: "bundle_events",
|
|
1476
|
+
where: [
|
|
1477
|
+
...filterWhere,
|
|
1478
|
+
{
|
|
1479
|
+
field: "received_at_ms",
|
|
1480
|
+
value: input.after.receivedAtMs
|
|
1481
|
+
},
|
|
1482
|
+
{
|
|
1483
|
+
field: "id",
|
|
1484
|
+
operator: "lt",
|
|
1485
|
+
value: input.after.id
|
|
1486
|
+
}
|
|
1487
|
+
],
|
|
1488
|
+
orderBy: [{
|
|
1489
|
+
field: "id",
|
|
1490
|
+
direction: "desc"
|
|
1491
|
+
}],
|
|
1492
|
+
limit: input.limit,
|
|
1493
|
+
offset: 0
|
|
1494
|
+
});
|
|
1495
|
+
if (sameTimestamp.length === input.limit) return sameTimestamp;
|
|
1496
|
+
const older = await crud.findMany({
|
|
1497
|
+
model: "bundle_events",
|
|
1498
|
+
where: [...where, {
|
|
1499
|
+
field: "received_at_ms",
|
|
1500
|
+
operator: "lt",
|
|
1501
|
+
value: input.after?.receivedAtMs ?? input.beforeReceivedAtMs
|
|
1502
|
+
}],
|
|
1503
|
+
orderBy: [{
|
|
1504
|
+
field: "received_at_ms",
|
|
1505
|
+
direction: "desc"
|
|
1506
|
+
}, {
|
|
1507
|
+
field: "id",
|
|
1508
|
+
direction: "desc"
|
|
1509
|
+
}],
|
|
1510
|
+
limit: input.limit - sameTimestamp.length,
|
|
1511
|
+
offset: 0
|
|
1512
|
+
});
|
|
1513
|
+
return [...sameTimestamp, ...older];
|
|
1514
|
+
};
|
|
1534
1515
|
var DatabaseAtomicCommitUnsupportedError = class extends Error {
|
|
1516
|
+
pluginName;
|
|
1535
1517
|
name = "DatabaseAtomicCommitUnsupportedError";
|
|
1536
1518
|
constructor(pluginName) {
|
|
1537
1519
|
super(`Database plugin "${pluginName}" cannot atomically commit changes across models.`);
|
|
@@ -1552,6 +1534,7 @@ var DatabaseRowReferencedError = class extends Error {
|
|
|
1552
1534
|
}
|
|
1553
1535
|
};
|
|
1554
1536
|
var DatabaseCommitConflictError = class extends Error {
|
|
1537
|
+
result;
|
|
1555
1538
|
name = "DatabaseCommitConflictError";
|
|
1556
1539
|
constructor(result) {
|
|
1557
1540
|
super("Database commit precondition failed.");
|
|
@@ -1779,12 +1762,6 @@ const applyChange = async (database, change, changeIndex) => {
|
|
|
1779
1762
|
}
|
|
1780
1763
|
return;
|
|
1781
1764
|
}
|
|
1782
|
-
case "analytics":
|
|
1783
|
-
await database.create({
|
|
1784
|
-
model: "bundle_events",
|
|
1785
|
-
data: change.row
|
|
1786
|
-
});
|
|
1787
|
-
return;
|
|
1788
1765
|
case "apiKeys": switch (change.operation) {
|
|
1789
1766
|
case "insert":
|
|
1790
1767
|
await database.create({
|
|
@@ -1856,10 +1833,10 @@ const hasOnlyKeys = (value, keys) => {
|
|
|
1856
1833
|
return Reflect.ownKeys(value).length === keys.length && keys.every((key) => Object.hasOwn(value, key));
|
|
1857
1834
|
};
|
|
1858
1835
|
const validateWhere = (where, field, validateValue = (value) => typeof value === "string") => {
|
|
1859
|
-
if (!isRecord$
|
|
1836
|
+
if (!isRecord$3(where) || !hasOnlyKeys(where, [field]) || !validateValue(Reflect.get(where, field))) throw new DatabasePluginInputError("invalid-data");
|
|
1860
1837
|
};
|
|
1861
1838
|
const validateDatabaseChange = (change) => {
|
|
1862
|
-
if (!isRecord$
|
|
1839
|
+
if (!isRecord$3(change)) throw new DatabasePluginInputError("invalid-data");
|
|
1863
1840
|
switch (change.model) {
|
|
1864
1841
|
case "bundles": switch (change.operation) {
|
|
1865
1842
|
case "insert":
|
|
@@ -1966,14 +1943,6 @@ const validateDatabaseChange = (change) => {
|
|
|
1966
1943
|
return;
|
|
1967
1944
|
default: throw new DatabasePluginInputError("invalid-operation");
|
|
1968
1945
|
}
|
|
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
1946
|
case "apiKeys": switch (change.operation) {
|
|
1978
1947
|
case "insert":
|
|
1979
1948
|
if (!hasOnlyKeys(change, [
|
|
@@ -1992,7 +1961,7 @@ const validateDatabaseChange = (change) => {
|
|
|
1992
1961
|
"update"
|
|
1993
1962
|
])) throw new DatabasePluginInputError("invalid-data");
|
|
1994
1963
|
validateWhere(change.where, "id");
|
|
1995
|
-
if (!isRecord$
|
|
1964
|
+
if (!isRecord$3(change.update)) throw new DatabasePluginInputError("invalid-data");
|
|
1996
1965
|
if (!hasOnlyKeys(change.update, ["revokedAtMs"])) throw new DatabasePluginInputError("invalid-data");
|
|
1997
1966
|
validateApiKeyUpdateData({ revoked_at_ms: change.update.revokedAtMs });
|
|
1998
1967
|
return;
|
|
@@ -2002,7 +1971,7 @@ const validateDatabaseChange = (change) => {
|
|
|
2002
1971
|
}
|
|
2003
1972
|
};
|
|
2004
1973
|
const validateDatabaseCommitExpectation = (expectation) => {
|
|
2005
|
-
if (!isRecord$
|
|
1974
|
+
if (!isRecord$3(expectation)) throw new DatabasePluginInputError("invalid-data");
|
|
2006
1975
|
if (expectation.model === "releases") {
|
|
2007
1976
|
if (!hasOnlyKeys(expectation, [
|
|
2008
1977
|
"model",
|
|
@@ -2022,7 +1991,7 @@ const validateDatabaseCommitExpectation = (expectation) => {
|
|
|
2022
1991
|
throw new DatabasePluginInputError("invalid-model");
|
|
2023
1992
|
};
|
|
2024
1993
|
function validateDatabaseCommit(input) {
|
|
2025
|
-
if (!isRecord$
|
|
1994
|
+
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
1995
|
input.changes.forEach(validateDatabaseChange);
|
|
2027
1996
|
if (Array.isArray(input.expectations)) input.expectations.forEach(validateDatabaseCommitExpectation);
|
|
2028
1997
|
}
|
|
@@ -2241,37 +2210,32 @@ const createDatabasePluginAdapter = (name, implementation) => {
|
|
|
2241
2210
|
return result;
|
|
2242
2211
|
}
|
|
2243
2212
|
},
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2213
|
+
insights: {
|
|
2214
|
+
recordEvent: (input) => implementation.recordInsights(input),
|
|
2215
|
+
async listEvents(input) {
|
|
2216
|
+
const ranges = await Promise.all(toInsightsEventRanges(input.filter).map((where) => listInsightsEventRange(crud, input, where)));
|
|
2217
|
+
if (ranges.length === 1) return ranges[0];
|
|
2218
|
+
return ranges.flat().sort((left, right) => right.received_at_ms - left.received_at_ms || compareInsightsText(right.id, left.id)).slice(0, input.limit);
|
|
2250
2219
|
},
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2220
|
+
findLatestEvents: (input) => implementation.findLatestInsightsEvents(input),
|
|
2221
|
+
countLatestEvents: (input) => implementation.countLatestInsightsEvents(input),
|
|
2222
|
+
countEvents(input) {
|
|
2223
|
+
return crud.count({
|
|
2224
|
+
model: "bundle_events",
|
|
2225
|
+
where: [
|
|
2226
|
+
...toInsightsBundleWhere(input.filter),
|
|
2227
|
+
{
|
|
2228
|
+
field: "received_at_ms",
|
|
2229
|
+
operator: "gte",
|
|
2230
|
+
value: input.sinceMs
|
|
2231
|
+
},
|
|
2232
|
+
{
|
|
2257
2233
|
field: "received_at_ms",
|
|
2258
2234
|
operator: "lt",
|
|
2259
2235
|
value: input.beforeReceivedAtMs
|
|
2260
|
-
}
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
direction: "asc"
|
|
2264
|
-
}, {
|
|
2265
|
-
field: "id",
|
|
2266
|
-
direction: "asc"
|
|
2267
|
-
}],
|
|
2268
|
-
limit: PAGE_SIZE$1,
|
|
2269
|
-
offset
|
|
2270
|
-
});
|
|
2271
|
-
rows.push(...page.filter((row) => input.after === void 0 || row.received_at_ms > input.after.receivedAtMs || row.received_at_ms === input.after.receivedAtMs && row.id > input.after.id));
|
|
2272
|
-
if (page.length < PAGE_SIZE$1) break;
|
|
2273
|
-
}
|
|
2274
|
-
return rows.slice(0, input.limit);
|
|
2236
|
+
}
|
|
2237
|
+
]
|
|
2238
|
+
});
|
|
2275
2239
|
}
|
|
2276
2240
|
},
|
|
2277
2241
|
apiKeys: {
|
|
@@ -2315,7 +2279,13 @@ const createDatabasePluginAdapter = (name, implementation) => {
|
|
|
2315
2279
|
...implementation.dispose ? { dispose: implementation.dispose } : {}
|
|
2316
2280
|
};
|
|
2317
2281
|
};
|
|
2318
|
-
const createDatabasePlugin = (options) => ({
|
|
2282
|
+
const createDatabasePlugin = (options) => ({
|
|
2283
|
+
...options,
|
|
2284
|
+
models: {
|
|
2285
|
+
...options.models,
|
|
2286
|
+
insights: createValidatedInsightsModel(options.models.insights)
|
|
2287
|
+
}
|
|
2288
|
+
});
|
|
2319
2289
|
//#endregion
|
|
2320
2290
|
//#region ../plugin-core/dist/createStoragePlugin.mjs
|
|
2321
2291
|
const createStoragePlugin = (options) => ({ ...options });
|
|
@@ -2450,6 +2420,8 @@ const rowsToBundles = (bundleRows, patchRows, referencedBundleRows) => {
|
|
|
2450
2420
|
//#endregion
|
|
2451
2421
|
//#region ../plugin-core/dist/databaseClientUpdates.mjs
|
|
2452
2422
|
var DatabasePatchUpdateUnsupportedError = class extends Error {
|
|
2423
|
+
bundleId;
|
|
2424
|
+
pluginName;
|
|
2453
2425
|
name = "DatabasePatchUpdateUnsupportedError";
|
|
2454
2426
|
constructor(bundleId, pluginName) {
|
|
2455
2427
|
super(`Database plugin "${pluginName}" cannot atomically replace patches for bundle "${bundleId}".`);
|
|
@@ -2538,7 +2510,8 @@ const hydrateRows = async (database, ownerRows) => {
|
|
|
2538
2510
|
const patchRows = await database.models.bundlePatches.findByBundleIds(ownerRows.map(({ id }) => id));
|
|
2539
2511
|
const ownerIds = new Set(ownerRows.map(({ id }) => id));
|
|
2540
2512
|
const referencedIds = [...new Set(patchRows.map(({ base_bundle_id }) => base_bundle_id).filter((id) => !ownerIds.has(id)))];
|
|
2541
|
-
|
|
2513
|
+
const referencedRows = referencedIds.length === 0 ? [] : await loadBundleRows(database, { id: { in: referencedIds } });
|
|
2514
|
+
return rowsToBundles(ownerRows, patchRows, referencedRows);
|
|
2542
2515
|
};
|
|
2543
2516
|
const cursorIdFilter = (cursor, direction) => {
|
|
2544
2517
|
if (cursor?.after) return { [direction === "desc" ? "lt" : "gt"]: cursor.after };
|
|
@@ -2590,6 +2563,7 @@ const responsePage = async (database, options) => {
|
|
|
2590
2563
|
//#endregion
|
|
2591
2564
|
//#region ../plugin-core/dist/databaseClient.mjs
|
|
2592
2565
|
var DatabaseBundleNotFoundError = class extends Error {
|
|
2566
|
+
bundleId;
|
|
2593
2567
|
name = "DatabaseBundleNotFoundError";
|
|
2594
2568
|
constructor(bundleId) {
|
|
2595
2569
|
super(`Bundle "${bundleId}" was not found.`);
|
|
@@ -2597,6 +2571,8 @@ var DatabaseBundleNotFoundError = class extends Error {
|
|
|
2597
2571
|
}
|
|
2598
2572
|
};
|
|
2599
2573
|
var DatabasePatchInsertUnsupportedError = class extends Error {
|
|
2574
|
+
bundleId;
|
|
2575
|
+
pluginName;
|
|
2600
2576
|
name = "DatabasePatchInsertUnsupportedError";
|
|
2601
2577
|
constructor(bundleId, pluginName) {
|
|
2602
2578
|
super(`Database plugin "${pluginName}" cannot atomically insert patches for bundle "${bundleId}".`);
|
|
@@ -2615,12 +2591,13 @@ const insertChanges = (bundle) => [{
|
|
|
2615
2591
|
}))];
|
|
2616
2592
|
const updateChanges = (bundleId, update) => {
|
|
2617
2593
|
const rowUpdate = bundleUpdateToRow(update);
|
|
2594
|
+
const patchesPresent = Object.hasOwn(update, "patches");
|
|
2618
2595
|
return [{
|
|
2619
2596
|
model: "bundles",
|
|
2620
2597
|
operation: "update",
|
|
2621
2598
|
where: { id: bundleId },
|
|
2622
2599
|
update: rowUpdate
|
|
2623
|
-
}, ...
|
|
2600
|
+
}, ...patchesPresent ? [{
|
|
2624
2601
|
model: "bundlePatches",
|
|
2625
2602
|
operation: "delete",
|
|
2626
2603
|
where: { bundleId }
|
|
@@ -2693,7 +2670,7 @@ const createDatabaseClient = (plugin) => {
|
|
|
2693
2670
|
};
|
|
2694
2671
|
};
|
|
2695
2672
|
//#endregion
|
|
2696
|
-
//#region ../plugin-core/dist/node_modules/.pnpm/verkit@0.
|
|
2673
|
+
//#region ../plugin-core/dist/node_modules/.pnpm/verkit@0.4.0/node_modules/verkit/dist/comparison-DenM3wCn.mjs
|
|
2697
2674
|
const LETTER_DASH_NUMBER = "[a-zA-Z0-9-]";
|
|
2698
2675
|
const NUMERIC_IDENTIFIER = String.raw`0|[1-9]\d*`;
|
|
2699
2676
|
const NUMERIC_IDENTIFIER_LOOSE = String.raw`\d+`;
|
|
@@ -2730,22 +2707,9 @@ function makeSafeRegexSource(source) {
|
|
|
2730
2707
|
function safeRegex(source, flags) {
|
|
2731
2708
|
return new RegExp(makeSafeRegexSource(source), flags);
|
|
2732
2709
|
}
|
|
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
2710
|
const FULL = safeRegex(`^${FULL_PLAIN}$`);
|
|
2743
2711
|
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+$/;
|
|
2712
|
+
const NUMERIC$1 = /^\d+$/;
|
|
2749
2713
|
function formatComparableVersion(version) {
|
|
2750
2714
|
const base = `${version.major}.${version.minor}.${version.patch}`;
|
|
2751
2715
|
return version.prerelease?.length ? `${base}-${version.prerelease.join(".")}` : base;
|
|
@@ -2766,7 +2730,7 @@ function parse(version, options = {}) {
|
|
|
2766
2730
|
if (minor > Number.MAX_SAFE_INTEGER || minor < 0) throw new TypeError(`Invalid minor version: ${match[2]}`);
|
|
2767
2731
|
if (patch > Number.MAX_SAFE_INTEGER || patch < 0) throw new TypeError(`Invalid patch version: ${match[3]}`);
|
|
2768
2732
|
const prerelease = match[4] ? match[4].split(".").map((identifier) => {
|
|
2769
|
-
if (NUMERIC.test(identifier)) {
|
|
2733
|
+
if (NUMERIC$1.test(identifier)) {
|
|
2770
2734
|
const numeric = Number(identifier);
|
|
2771
2735
|
if (numeric >= 0 && numeric < Number.MAX_SAFE_INTEGER) return numeric;
|
|
2772
2736
|
}
|
|
@@ -2787,11 +2751,20 @@ function tryParse(version, options = {}) {
|
|
|
2787
2751
|
return null;
|
|
2788
2752
|
}
|
|
2789
2753
|
}
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
const
|
|
2754
|
+
const NUMERIC = /^\d+$/;
|
|
2755
|
+
function compareIdentifiers(left, right) {
|
|
2756
|
+
if (typeof left === "number" && typeof right === "number") return left === right ? 0 : left < right ? -1 : 1;
|
|
2757
|
+
const leftNumeric = NUMERIC.test(String(left));
|
|
2758
|
+
const rightNumeric = NUMERIC.test(String(right));
|
|
2759
|
+
const normalizedLeft = leftNumeric ? Number(left) : left;
|
|
2760
|
+
const normalizedRight = rightNumeric ? Number(right) : right;
|
|
2761
|
+
return normalizedLeft === normalizedRight ? 0 : leftNumeric && !rightNumeric ? -1 : rightNumeric && !leftNumeric ? 1 : normalizedLeft < normalizedRight ? -1 : 1;
|
|
2762
|
+
}
|
|
2763
|
+
function compareMainParsed(left, right) {
|
|
2764
|
+
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;
|
|
2765
|
+
}
|
|
2766
|
+
function comparePrereleaseParsed(left, right) {
|
|
2767
|
+
const leftPrerelease = left.prerelease;
|
|
2795
2768
|
const rightPrerelease = right.prerelease;
|
|
2796
2769
|
if (leftPrerelease?.length && !rightPrerelease?.length) return -1;
|
|
2797
2770
|
if (!leftPrerelease?.length && rightPrerelease?.length) return 1;
|
|
@@ -2808,26 +2781,15 @@ function comparePrereleaseParsed(left, right) {
|
|
|
2808
2781
|
function compareParsed(left, right) {
|
|
2809
2782
|
return compareMainParsed(left, right) || comparePrereleaseParsed(left, right);
|
|
2810
2783
|
}
|
|
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);
|
|
2784
|
+
function compare$1(left, right, options = {}) {
|
|
2785
|
+
return compareParsed(parse(left, options), parse(right, options));
|
|
2827
2786
|
}
|
|
2787
|
+
//#endregion
|
|
2788
|
+
//#region ../plugin-core/dist/node_modules/.pnpm/verkit@0.4.0/node_modules/verkit/dist/set-CC5YeoYX.mjs
|
|
2828
2789
|
const STRICT_COMPARATOR = safeRegex(String.raw`^${GREATER_LESS_THAN}\s*(${FULL_PLAIN})$|^$`);
|
|
2829
2790
|
const LOOSE_COMPARATOR$1 = safeRegex(String.raw`^${GREATER_LESS_THAN}\s*(${LOOSE_PLAIN})$|^$`);
|
|
2830
2791
|
function parseComparator(comparator, options = {}) {
|
|
2792
|
+
if (typeof comparator !== "string") return comparator;
|
|
2831
2793
|
const normalized = comparator.trim().replaceAll(/\s+/g, " ");
|
|
2832
2794
|
const match = normalized.match(options.loose ? LOOSE_COMPARATOR$1 : STRICT_COMPARATOR);
|
|
2833
2795
|
if (!match) throw new TypeError(`Invalid comparator: ${normalized}`);
|
|
@@ -2840,9 +2802,8 @@ function parseComparator(comparator, options = {}) {
|
|
|
2840
2802
|
version
|
|
2841
2803
|
};
|
|
2842
2804
|
}
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
}
|
|
2805
|
+
//#endregion
|
|
2806
|
+
//#region ../plugin-core/dist/node_modules/.pnpm/verkit@0.4.0/node_modules/verkit/dist/range-DvX-Y6iv.mjs
|
|
2846
2807
|
const BUILD_STRIP = new RegExp(BUILD, "g");
|
|
2847
2808
|
const BUILD_SAFE = safeRegex(BUILD);
|
|
2848
2809
|
const STRICT_HYPHEN = safeRegex(String.raw`^\s*(${XRANGE_PLAIN})\s+-\s+(${XRANGE_PLAIN})\s*$`);
|
|
@@ -2968,9 +2929,9 @@ function parseSimpleRange(input, options) {
|
|
|
2968
2929
|
function parseRange(range, options = {}) {
|
|
2969
2930
|
if (typeof range !== "string") return range;
|
|
2970
2931
|
const parsedOptions = { ...options };
|
|
2971
|
-
const
|
|
2972
|
-
let sets =
|
|
2973
|
-
if (!sets.length) throw new TypeError(`Range contains no valid comparator sets: ${
|
|
2932
|
+
const normalizedRange = range.trim().replaceAll(/\s+/g, " ");
|
|
2933
|
+
let sets = normalizedRange.split("||").map((part) => parseSimpleRange(part.trim(), parsedOptions)).filter((set) => set.length);
|
|
2934
|
+
if (!sets.length) throw new TypeError(`Range contains no valid comparator sets: ${normalizedRange}`);
|
|
2974
2935
|
if (sets.length > 1) {
|
|
2975
2936
|
const first = sets[0];
|
|
2976
2937
|
sets = sets.filter((set) => set[0]?.value !== "<0.0.0-0");
|
|
@@ -2981,19 +2942,39 @@ function parseRange(range, options = {}) {
|
|
|
2981
2942
|
}
|
|
2982
2943
|
}
|
|
2983
2944
|
return {
|
|
2984
|
-
normalized: sets.map((set) => set.map((comparator) => comparator.value).join(" ")).join("||"),
|
|
2985
2945
|
options: parsedOptions,
|
|
2986
|
-
raw,
|
|
2987
2946
|
sets
|
|
2988
2947
|
};
|
|
2989
2948
|
}
|
|
2949
|
+
//#endregion
|
|
2950
|
+
//#region ../plugin-core/dist/node_modules/.pnpm/verkit@0.4.0/node_modules/verkit/dist/version-CQ98ZBpL.mjs
|
|
2951
|
+
const COERCE_EXACT = safeRegex(COERCE);
|
|
2952
|
+
const COERCE_FULL_EXACT = safeRegex(COERCE_FULL);
|
|
2953
|
+
safeRegex(`^${PRERELEASE}$`);
|
|
2954
|
+
safeRegex(`^${PRERELEASE_LOOSE}$`);
|
|
2955
|
+
function normalizeFull(version, options = {}) {
|
|
2956
|
+
const parsed = tryParse(version, options);
|
|
2957
|
+
return parsed ? formatFullVersion(parsed) : null;
|
|
2958
|
+
}
|
|
2990
2959
|
function normalize(version, options = {}) {
|
|
2991
2960
|
const parsed = tryParse(version, options);
|
|
2992
2961
|
return parsed ? formatComparableVersion(parsed) : null;
|
|
2993
2962
|
}
|
|
2994
2963
|
function coerce(value, options = {}) {
|
|
2995
|
-
|
|
2996
|
-
|
|
2964
|
+
if (typeof value === "object") return value;
|
|
2965
|
+
const input = typeof value === "number" ? String(value) : value;
|
|
2966
|
+
let match = null;
|
|
2967
|
+
if (options.rtl) {
|
|
2968
|
+
const expression = safeRegex(options.includePrerelease ? COERCE_FULL : COERCE, "g");
|
|
2969
|
+
let next;
|
|
2970
|
+
while ((next = expression.exec(input)) && (!match || match.index + match[0].length !== input.length)) {
|
|
2971
|
+
if (!match || next.index + next[0].length !== match.index + match[0].length) match = next;
|
|
2972
|
+
expression.lastIndex = next.index + next[1].length + next[2].length;
|
|
2973
|
+
}
|
|
2974
|
+
} else match = (options.includePrerelease ? COERCE_FULL_EXACT : COERCE_EXACT).exec(input);
|
|
2975
|
+
if (!match) return null;
|
|
2976
|
+
const major = match[2];
|
|
2977
|
+
return tryParse(`${major}.${match[3] || "0"}.${match[4] || "0"}${options.includePrerelease && match[5] ? `-${match[5]}` : ""}${options.includePrerelease && match[6] ? `+${match[6]}` : ""}`, options);
|
|
2997
2978
|
}
|
|
2998
2979
|
//#endregion
|
|
2999
2980
|
//#region ../plugin-core/dist/releaseCatalogCompiler.mjs
|
|
@@ -3273,15 +3254,16 @@ function compileAppVersion(releases) {
|
|
|
3273
3254
|
const retainedIds = new Set(segmentReleases.flatMap(({ retainedIds, rollbackIds }) => [...retainedIds, ...rollbackIds]));
|
|
3274
3255
|
const retainedReleases = releases.filter((release) => retainedIds.has(release.id));
|
|
3275
3256
|
const descriptorIndex = new Map(retainedReleases.map((release, index) => [release.id, index]));
|
|
3257
|
+
const segments = mergeSegments(segmentReleases.map(({ segment, retainedIds, rollbackIds }) => ({
|
|
3258
|
+
...segment,
|
|
3259
|
+
releaseIndexes: releases.filter((release) => retainedIds.has(release.id)).map((release) => descriptorIndex.get(release.id)).filter((index) => index !== void 0),
|
|
3260
|
+
rollbackReleaseIndexes: releases.filter((release) => rollbackIds.has(release.id)).map((release) => descriptorIndex.get(release.id)).filter((index) => index !== void 0)
|
|
3261
|
+
})).filter((segment) => segment.releaseIndexes.length > 0 || segment.rollbackReleaseIndexes.length > 0));
|
|
3276
3262
|
return {
|
|
3277
3263
|
envelope: {
|
|
3278
3264
|
fallbackPolicy: RELEASE_CATALOG_FALLBACK_POLICY,
|
|
3279
3265
|
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)),
|
|
3266
|
+
segments,
|
|
3285
3267
|
strategy: "APP_VERSION"
|
|
3286
3268
|
},
|
|
3287
3269
|
retainedReleases
|
|
@@ -3349,7 +3331,8 @@ function versionInSegment(version, segment) {
|
|
|
3349
3331
|
return true;
|
|
3350
3332
|
}
|
|
3351
3333
|
function canonicalizeAppVersion(appVersion) {
|
|
3352
|
-
|
|
3334
|
+
const version = coerce(appVersion);
|
|
3335
|
+
return version ? normalizeFull(version) : null;
|
|
3353
3336
|
}
|
|
3354
3337
|
function projectCompiledCatalog(catalog, appVersion) {
|
|
3355
3338
|
let indexes;
|
|
@@ -3377,6 +3360,7 @@ function projectCompiledRollbackCatalog(catalog, appVersion) {
|
|
|
3377
3360
|
const RELEASE_PAGE_SIZE = 1e3;
|
|
3378
3361
|
const DEFAULT_MAX_ATTEMPTS = 3;
|
|
3379
3362
|
var ReleaseCatalogMutationError = class extends Error {
|
|
3363
|
+
code;
|
|
3380
3364
|
name = "ReleaseCatalogMutationError";
|
|
3381
3365
|
constructor(code, message) {
|
|
3382
3366
|
super(message);
|
|
@@ -3636,6 +3620,7 @@ async function rebuildReleaseCatalog(input) {
|
|
|
3636
3620
|
//#endregion
|
|
3637
3621
|
//#region ../plugin-core/dist/releaseManagement.mjs
|
|
3638
3622
|
var ReleaseManagementError = class extends Error {
|
|
3623
|
+
code;
|
|
3639
3624
|
name = "ReleaseManagementError";
|
|
3640
3625
|
constructor(code, message) {
|
|
3641
3626
|
super(message);
|
|
@@ -3715,7 +3700,7 @@ async function deleteRelease(input) {
|
|
|
3715
3700
|
}
|
|
3716
3701
|
//#endregion
|
|
3717
3702
|
//#region ../plugin-core/dist/storageDownloadPath.mjs
|
|
3718
|
-
const decodeBase64Url = (value) => {
|
|
3703
|
+
const decodeBase64Url$1 = (value) => {
|
|
3719
3704
|
try {
|
|
3720
3705
|
const base64 = value.replaceAll("-", "+").replaceAll("_", "/");
|
|
3721
3706
|
const binary = atob(base64.padEnd(Math.ceil(base64.length / 4) * 4, "="));
|
|
@@ -3727,7 +3712,7 @@ const decodeBase64Url = (value) => {
|
|
|
3727
3712
|
const parseStorageDownloadPath = (path) => {
|
|
3728
3713
|
const match = /^\/storage\/([^/]+)\/([^/]+)$/.exec(path);
|
|
3729
3714
|
if (!match) return null;
|
|
3730
|
-
const storageUri = decodeBase64Url(match[1]);
|
|
3715
|
+
const storageUri = decodeBase64Url$1(match[1]);
|
|
3731
3716
|
if (storageUri === null) return null;
|
|
3732
3717
|
try {
|
|
3733
3718
|
return {
|
|
@@ -3848,7 +3833,7 @@ const createReleaseCatalogRouteHandlers = (clientAccessHeaderName = "x-api-key")
|
|
|
3848
3833
|
//#endregion
|
|
3849
3834
|
//#region ../../packages/server/dist/handlerReleaseManagementRoutes.mjs
|
|
3850
3835
|
const unavailable = () => Response.json({ error: "Not found" }, { status: 404 });
|
|
3851
|
-
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3836
|
+
const isRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3852
3837
|
const parseRevision = (value) => {
|
|
3853
3838
|
if (value === void 0 || value === null || value === "") return void 0;
|
|
3854
3839
|
const revision = typeof value === "number" ? value : Number(value);
|
|
@@ -3857,7 +3842,7 @@ const parseRevision = (value) => {
|
|
|
3857
3842
|
};
|
|
3858
3843
|
const parsePolicyInput = async (request) => {
|
|
3859
3844
|
const body = await request.json();
|
|
3860
|
-
if (!isRecord(body) || !isRecord(body.patch)) throw new HandlerBadRequestError("Invalid Release policy mutation");
|
|
3845
|
+
if (!isRecord$2(body) || !isRecord$2(body.patch)) throw new HandlerBadRequestError("Invalid Release policy mutation");
|
|
3861
3846
|
return {
|
|
3862
3847
|
expectedRevision: parseRevision(body.expectedRevision),
|
|
3863
3848
|
patch: body.patch
|
|
@@ -3966,6 +3951,311 @@ const createReleaseManagementRouteHandlers = () => ({
|
|
|
3966
3951
|
}
|
|
3967
3952
|
});
|
|
3968
3953
|
//#endregion
|
|
3954
|
+
//#region ../../packages/server/dist/insights/errors.mjs
|
|
3955
|
+
var InsightsBadRequestError = class extends Error {
|
|
3956
|
+
name = "InsightsBadRequestError";
|
|
3957
|
+
};
|
|
3958
|
+
var InsightsPayloadTooLargeError = class extends Error {
|
|
3959
|
+
maximumBytes;
|
|
3960
|
+
name = "InsightsPayloadTooLargeError";
|
|
3961
|
+
constructor(maximumBytes) {
|
|
3962
|
+
super(`Event payload exceeds ${maximumBytes} bytes`);
|
|
3963
|
+
this.maximumBytes = maximumBytes;
|
|
3964
|
+
}
|
|
3965
|
+
};
|
|
3966
|
+
//#endregion
|
|
3967
|
+
//#region ../../packages/server/dist/insights/eventInput.mjs
|
|
3968
|
+
const MAX_EVENT_STRING_LENGTH = 1024;
|
|
3969
|
+
const MAX_IDENTITY_LENGTH$2 = 255;
|
|
3970
|
+
const EVENT_BODY_MAX_BYTES = 16384;
|
|
3971
|
+
const eventKeys = /* @__PURE__ */ new Set([
|
|
3972
|
+
"type",
|
|
3973
|
+
"installId",
|
|
3974
|
+
"toBundleId",
|
|
3975
|
+
"userId",
|
|
3976
|
+
"username",
|
|
3977
|
+
"platform",
|
|
3978
|
+
"appVersion",
|
|
3979
|
+
"channel",
|
|
3980
|
+
"cohort",
|
|
3981
|
+
"fingerprintHash",
|
|
3982
|
+
"fromBundleId",
|
|
3983
|
+
"fromReleaseId",
|
|
3984
|
+
"toReleaseId",
|
|
3985
|
+
"updateStrategy",
|
|
3986
|
+
"sdkVersion"
|
|
3987
|
+
]);
|
|
3988
|
+
function isRecord$1(value) {
|
|
3989
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3990
|
+
}
|
|
3991
|
+
function requireStringField(payload, key) {
|
|
3992
|
+
const value = payload[key];
|
|
3993
|
+
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}`);
|
|
3994
|
+
return value;
|
|
3995
|
+
}
|
|
3996
|
+
function requireNullableStringField(payload, key) {
|
|
3997
|
+
if (payload[key] === null) return null;
|
|
3998
|
+
return requireStringField(payload, key);
|
|
3999
|
+
}
|
|
4000
|
+
function requireIdentityField(payload, key) {
|
|
4001
|
+
const value = requireStringField(payload, key);
|
|
4002
|
+
if (value.length > MAX_IDENTITY_LENGTH$2) throw new InsightsBadRequestError(`Invalid event field: ${key}`);
|
|
4003
|
+
return value;
|
|
4004
|
+
}
|
|
4005
|
+
async function readBoundedText(request) {
|
|
4006
|
+
const contentLength = request.headers.get("content-length");
|
|
4007
|
+
const declaredByteLength = Number(contentLength);
|
|
4008
|
+
if (contentLength !== null && Number.isSafeInteger(declaredByteLength) && declaredByteLength > 16384) throw new InsightsPayloadTooLargeError(EVENT_BODY_MAX_BYTES);
|
|
4009
|
+
if (request.body === null) return "";
|
|
4010
|
+
const reader = request.body.getReader();
|
|
4011
|
+
const decoder = new TextDecoder();
|
|
4012
|
+
let byteLength = 0;
|
|
4013
|
+
let text = "";
|
|
4014
|
+
while (true) {
|
|
4015
|
+
const result = await reader.read();
|
|
4016
|
+
if (result.done) break;
|
|
4017
|
+
byteLength += result.value.byteLength;
|
|
4018
|
+
if (byteLength > 16384) {
|
|
4019
|
+
await reader.cancel();
|
|
4020
|
+
throw new InsightsPayloadTooLargeError(EVENT_BODY_MAX_BYTES);
|
|
4021
|
+
}
|
|
4022
|
+
text += decoder.decode(result.value, { stream: true });
|
|
4023
|
+
}
|
|
4024
|
+
return text + decoder.decode();
|
|
4025
|
+
}
|
|
4026
|
+
async function parseJson(request) {
|
|
4027
|
+
const text = await readBoundedText(request);
|
|
4028
|
+
try {
|
|
4029
|
+
return JSON.parse(text);
|
|
4030
|
+
} catch (error) {
|
|
4031
|
+
if (error instanceof SyntaxError) throw new InsightsBadRequestError("Invalid event payload");
|
|
4032
|
+
throw error;
|
|
4033
|
+
}
|
|
4034
|
+
}
|
|
4035
|
+
function requireEvent(payload) {
|
|
4036
|
+
if (!isRecord$1(payload) || Object.keys(payload).some((key) => !eventKeys.has(key))) throw new InsightsBadRequestError("Invalid event payload");
|
|
4037
|
+
const platform = requireStringField(payload, "platform");
|
|
4038
|
+
if (platform !== "ios" && platform !== "android") throw new InsightsBadRequestError("Invalid event field: platform");
|
|
4039
|
+
const base = {
|
|
4040
|
+
installId: requireIdentityField(payload, "installId"),
|
|
4041
|
+
toBundleId: requireStringField(payload, "toBundleId"),
|
|
4042
|
+
...payload.userId === void 0 ? {} : { userId: requireIdentityField(payload, "userId") },
|
|
4043
|
+
...payload.username === void 0 ? {} : { username: requireStringField(payload, "username") },
|
|
4044
|
+
platform,
|
|
4045
|
+
appVersion: requireStringField(payload, "appVersion"),
|
|
4046
|
+
channel: requireStringField(payload, "channel"),
|
|
4047
|
+
cohort: requireStringField(payload, "cohort"),
|
|
4048
|
+
fingerprintHash: requireNullableStringField(payload, "fingerprintHash"),
|
|
4049
|
+
sdkVersion: payload.sdkVersion === void 0 ? null : requireNullableStringField(payload, "sdkVersion"),
|
|
4050
|
+
fromReleaseId: requireNullableStringField(payload, "fromReleaseId"),
|
|
4051
|
+
toReleaseId: requireNullableStringField(payload, "toReleaseId")
|
|
4052
|
+
};
|
|
4053
|
+
const type = requireStringField(payload, "type");
|
|
4054
|
+
switch (type) {
|
|
4055
|
+
case "UPDATE_DOWNLOADED":
|
|
4056
|
+
case "UPDATE_APPLIED":
|
|
4057
|
+
case "RECOVERED": {
|
|
4058
|
+
const updateStrategy = requireStringField(payload, "updateStrategy");
|
|
4059
|
+
if (updateStrategy !== "fingerprint" && updateStrategy !== "appVersion") throw new InsightsBadRequestError("Invalid event field: updateStrategy");
|
|
4060
|
+
return {
|
|
4061
|
+
...base,
|
|
4062
|
+
type,
|
|
4063
|
+
fromBundleId: requireStringField(payload, "fromBundleId"),
|
|
4064
|
+
updateStrategy
|
|
4065
|
+
};
|
|
4066
|
+
}
|
|
4067
|
+
case "UNCHANGED":
|
|
4068
|
+
if (payload.fromBundleId !== null || payload.updateStrategy !== null) throw new InsightsBadRequestError("Invalid unchanged event shape");
|
|
4069
|
+
return {
|
|
4070
|
+
...base,
|
|
4071
|
+
type,
|
|
4072
|
+
fromBundleId: null,
|
|
4073
|
+
updateStrategy: null
|
|
4074
|
+
};
|
|
4075
|
+
default: throw new InsightsBadRequestError("Invalid event field: type");
|
|
4076
|
+
}
|
|
4077
|
+
}
|
|
4078
|
+
async function parseBundleEventRequest(request) {
|
|
4079
|
+
return requireEvent(await parseJson(request));
|
|
4080
|
+
}
|
|
4081
|
+
function createBundleEventRow(input) {
|
|
4082
|
+
input = requireEvent(input);
|
|
4083
|
+
const base = {
|
|
4084
|
+
app_version: input.appVersion,
|
|
4085
|
+
channel: input.channel,
|
|
4086
|
+
from_release_id: input.fromReleaseId,
|
|
4087
|
+
id: createUUIDv7(),
|
|
4088
|
+
install_id: input.installId,
|
|
4089
|
+
platform: input.platform,
|
|
4090
|
+
received_at_ms: Date.now(),
|
|
4091
|
+
to_bundle_id: input.toBundleId,
|
|
4092
|
+
to_release_id: input.toReleaseId,
|
|
4093
|
+
user_id: input.userId ?? null,
|
|
4094
|
+
metadata: {
|
|
4095
|
+
cohort: input.cohort,
|
|
4096
|
+
fingerprint_hash: input.fingerprintHash,
|
|
4097
|
+
sdk_version: input.sdkVersion ?? null,
|
|
4098
|
+
username: input.username ?? null,
|
|
4099
|
+
update_strategy: input.updateStrategy
|
|
4100
|
+
}
|
|
4101
|
+
};
|
|
4102
|
+
switch (input.type) {
|
|
4103
|
+
case "UPDATE_DOWNLOADED":
|
|
4104
|
+
case "UPDATE_APPLIED":
|
|
4105
|
+
case "RECOVERED": return {
|
|
4106
|
+
...base,
|
|
4107
|
+
from_bundle_id: input.fromBundleId,
|
|
4108
|
+
type: input.type
|
|
4109
|
+
};
|
|
4110
|
+
case "UNCHANGED": return {
|
|
4111
|
+
...base,
|
|
4112
|
+
from_bundle_id: null,
|
|
4113
|
+
type: input.type
|
|
4114
|
+
};
|
|
4115
|
+
}
|
|
4116
|
+
}
|
|
4117
|
+
//#endregion
|
|
4118
|
+
//#region ../../packages/server/dist/insights/queryInput.mjs
|
|
4119
|
+
const MAX_PAGE_LIMIT$1 = 100;
|
|
4120
|
+
const MAX_IDENTITY_LENGTH$1 = 255;
|
|
4121
|
+
const MAX_CURSOR_LENGTH$1 = 8192;
|
|
4122
|
+
const readSingle = (url, key) => {
|
|
4123
|
+
const values = url.searchParams.getAll(key);
|
|
4124
|
+
if (values.length > 1) throw new InsightsBadRequestError(`Duplicate '${key}' query parameter.`);
|
|
4125
|
+
return values[0];
|
|
4126
|
+
};
|
|
4127
|
+
const readPageLimit = (url) => {
|
|
4128
|
+
const value = readSingle(url, "limit");
|
|
4129
|
+
if (value === void 0) return void 0;
|
|
4130
|
+
const limit = Number(value);
|
|
4131
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_PAGE_LIMIT$1) throw new InsightsBadRequestError("Invalid 'limit' query parameter.");
|
|
4132
|
+
return limit;
|
|
4133
|
+
};
|
|
4134
|
+
const readCursor = (url) => {
|
|
4135
|
+
const cursor = readSingle(url, "cursor");
|
|
4136
|
+
if (cursor !== void 0 && (cursor.length === 0 || cursor.length > MAX_CURSOR_LENGTH$1)) throw new InsightsBadRequestError("Invalid 'cursor' query parameter.");
|
|
4137
|
+
return cursor;
|
|
4138
|
+
};
|
|
4139
|
+
const readId = (url, key, maximumLength = MAX_IDENTITY_LENGTH$1) => {
|
|
4140
|
+
const value = readSingle(url, key);
|
|
4141
|
+
if (value === void 0 || value.length === 0 || value.length > maximumLength) throw new InsightsBadRequestError(`Invalid '${key}' query parameter.`);
|
|
4142
|
+
return value;
|
|
4143
|
+
};
|
|
4144
|
+
const readTimestamp = (url, key) => {
|
|
4145
|
+
const raw = readSingle(url, key);
|
|
4146
|
+
if (raw === void 0) return void 0;
|
|
4147
|
+
const value = Number(raw);
|
|
4148
|
+
if (!raw.length || !Number.isSafeInteger(value) || value < 0) throw new InsightsBadRequestError(`Invalid '${key}' query parameter.`);
|
|
4149
|
+
return value;
|
|
4150
|
+
};
|
|
4151
|
+
const readScope$1 = (url) => {
|
|
4152
|
+
const platform = readSingle(url, "platform");
|
|
4153
|
+
if (platform !== "ios" && platform !== "android") throw new InsightsBadRequestError("Invalid 'platform' query parameter.");
|
|
4154
|
+
return {
|
|
4155
|
+
platform,
|
|
4156
|
+
channel: readId(url, "channel", 1024)
|
|
4157
|
+
};
|
|
4158
|
+
};
|
|
4159
|
+
const parseEventPageInput = (request) => {
|
|
4160
|
+
const url = new URL(request.url);
|
|
4161
|
+
const bundleFields = [
|
|
4162
|
+
"bundleId",
|
|
4163
|
+
"outcome",
|
|
4164
|
+
"platform",
|
|
4165
|
+
"channel"
|
|
4166
|
+
];
|
|
4167
|
+
let bundle;
|
|
4168
|
+
if (bundleFields.some((key) => url.searchParams.has(key))) {
|
|
4169
|
+
const outcome = readSingle(url, "outcome");
|
|
4170
|
+
if (outcome !== "downloaded" && outcome !== "applied" && outcome !== "recovered" && outcome !== "unchanged") throw new InsightsBadRequestError("Invalid 'outcome' query parameter.");
|
|
4171
|
+
bundle = {
|
|
4172
|
+
...readScope$1(url),
|
|
4173
|
+
bundleId: readId(url, "bundleId", 1024),
|
|
4174
|
+
outcome
|
|
4175
|
+
};
|
|
4176
|
+
}
|
|
4177
|
+
return {
|
|
4178
|
+
beforeReceivedAtMs: readTimestamp(url, "beforeReceivedAtMs"),
|
|
4179
|
+
sinceMs: readTimestamp(url, "sinceMs"),
|
|
4180
|
+
cursor: readCursor(url),
|
|
4181
|
+
limit: readPageLimit(url),
|
|
4182
|
+
...bundle === void 0 ? {} : { bundle }
|
|
4183
|
+
};
|
|
4184
|
+
};
|
|
4185
|
+
const parseUserInstallationPageInput = (request) => {
|
|
4186
|
+
const url = new URL(request.url);
|
|
4187
|
+
const cursor = readCursor(url);
|
|
4188
|
+
const limit = readPageLimit(url);
|
|
4189
|
+
return {
|
|
4190
|
+
userId: readId(url, "userId"),
|
|
4191
|
+
...cursor === void 0 ? {} : { cursor },
|
|
4192
|
+
...limit === void 0 ? {} : { limit }
|
|
4193
|
+
};
|
|
4194
|
+
};
|
|
4195
|
+
const parseReportingOverviewInput = (request) => {
|
|
4196
|
+
const url = new URL(request.url);
|
|
4197
|
+
const window = readSingle(url, "window") ?? "30d";
|
|
4198
|
+
if (window !== "24h" && window !== "7d" && window !== "30d") throw new InsightsBadRequestError("Invalid 'window' query parameter.");
|
|
4199
|
+
const bundleId = readSingle(url, "bundleId");
|
|
4200
|
+
return {
|
|
4201
|
+
...readScope$1(url),
|
|
4202
|
+
window,
|
|
4203
|
+
...bundleId === void 0 ? {} : { bundleId: readId(url, "bundleId", 1024) }
|
|
4204
|
+
};
|
|
4205
|
+
};
|
|
4206
|
+
//#endregion
|
|
4207
|
+
//#region ../../packages/server/dist/insights/routes.mjs
|
|
4208
|
+
const json = (body, status) => Response.json(body, {
|
|
4209
|
+
headers: { "cache-control": "private, no-store" },
|
|
4210
|
+
status
|
|
4211
|
+
});
|
|
4212
|
+
const requireParam = (params, key) => {
|
|
4213
|
+
const value = params[key];
|
|
4214
|
+
if (value === void 0 || value.length === 0) throw new InsightsBadRequestError(`Missing route parameter: ${key}`);
|
|
4215
|
+
try {
|
|
4216
|
+
return decodeURIComponent(value);
|
|
4217
|
+
} catch {
|
|
4218
|
+
throw new InsightsBadRequestError(`Invalid route parameter: ${key}`);
|
|
4219
|
+
}
|
|
4220
|
+
};
|
|
4221
|
+
const run = async (operation) => {
|
|
4222
|
+
try {
|
|
4223
|
+
return await operation();
|
|
4224
|
+
} catch (error) {
|
|
4225
|
+
if (error instanceof InsightsBadRequestError) return json({ error: error.message }, 400);
|
|
4226
|
+
if (error instanceof InsightsPayloadTooLargeError) return json({ error: error.message }, 413);
|
|
4227
|
+
throw error;
|
|
4228
|
+
}
|
|
4229
|
+
};
|
|
4230
|
+
const query = (operation) => run(async () => json(await operation(), 200));
|
|
4231
|
+
const createInsightsRouteHandlers = (provider) => ({
|
|
4232
|
+
appendBundleEvent: async (_params, request) => run(async () => {
|
|
4233
|
+
await provider.appendBundleEvent(await parseBundleEventRequest(request));
|
|
4234
|
+
return new Response(null, { status: 204 });
|
|
4235
|
+
}),
|
|
4236
|
+
getReportingOverview: (_params, request) => query(() => provider.getReportingOverview(parseReportingOverviewInput(request))),
|
|
4237
|
+
getInstallation: (params) => run(async () => {
|
|
4238
|
+
const installation = await provider.getInstallation({ installId: requireParam(params, "installId") });
|
|
4239
|
+
return installation === null ? json({ error: "Installation not found" }, 404) : json(installation, 200);
|
|
4240
|
+
}),
|
|
4241
|
+
listEvents: (_params, request) => query(() => provider.listEvents(parseEventPageInput(request))),
|
|
4242
|
+
listInstallationEvents: (params, request) => query(() => provider.listInstallationEvents({
|
|
4243
|
+
...parseEventPageInput(request),
|
|
4244
|
+
installId: requireParam(params, "installId")
|
|
4245
|
+
})),
|
|
4246
|
+
pageInstallationsByCurrentUserId: (_params, request) => query(() => provider.pageInstallationsByCurrentUserId(parseUserInstallationPageInput(request)))
|
|
4247
|
+
});
|
|
4248
|
+
const registerInsightsClientRoutes = (add) => {
|
|
4249
|
+
add("POST", "/events", "appendBundleEvent");
|
|
4250
|
+
};
|
|
4251
|
+
const registerInsightsAdminRoutes = (add) => {
|
|
4252
|
+
add("GET", "/events", "listEvents");
|
|
4253
|
+
add("GET", "/overview", "getReportingOverview");
|
|
4254
|
+
add("GET", "/installations", "pageInstallationsByCurrentUserId");
|
|
4255
|
+
add("GET", "/installations/:installId/events", "listInstallationEvents");
|
|
4256
|
+
add("GET", "/installations/:installId", "getInstallation");
|
|
4257
|
+
};
|
|
4258
|
+
//#endregion
|
|
3969
4259
|
//#region ../../packages/server/dist/internalRouter.mjs
|
|
3970
4260
|
const normalizePath = (path) => {
|
|
3971
4261
|
if (!path) return "/";
|
|
@@ -4072,13 +4362,13 @@ const createDownloadStorageRouteHandler = (downloadStorageObject) => async (para
|
|
|
4072
4362
|
statusText: response.statusText
|
|
4073
4363
|
});
|
|
4074
4364
|
};
|
|
4075
|
-
function createHotUpdaterHandlers(api,
|
|
4365
|
+
function createHotUpdaterHandlers(api, insights, apiKeyAuth, downloadStorageObject) {
|
|
4076
4366
|
const routeHandlers = {
|
|
4077
4367
|
...createVersionRouteHandlers(),
|
|
4078
4368
|
...createReleaseCatalogRouteHandlers(apiKeyAuth?.headerName),
|
|
4079
4369
|
...createReleaseManagementRouteHandlers(),
|
|
4080
4370
|
...createBundleRouteHandlers(),
|
|
4081
|
-
...
|
|
4371
|
+
...insights === void 0 ? {} : createInsightsRouteHandlers(insights),
|
|
4082
4372
|
...downloadStorageObject === void 0 ? {} : { downloadStorageObject: createDownloadStorageRouteHandler(downloadStorageObject) }
|
|
4083
4373
|
};
|
|
4084
4374
|
const clientRouter = createRouter();
|
|
@@ -4088,7 +4378,7 @@ function createHotUpdaterHandlers(api, analytics, apiKeyAuth, downloadStorageObj
|
|
|
4088
4378
|
addClientRoute("GET", "/release-catalogs/app-version/:platform/:channelKey/:appVersion", "appVersionReleaseCatalog");
|
|
4089
4379
|
addClientRoute("GET", "/release-catalogs/fingerprint/:platform/:channelKey/:fingerprintHash", "fingerprintReleaseCatalog");
|
|
4090
4380
|
addClientRoute("GET", "/artifacts/:targetBundleId/from/:currentBundleId", "artifact");
|
|
4091
|
-
if (
|
|
4381
|
+
if (insights !== void 0) registerInsightsClientRoutes(addClientRoute);
|
|
4092
4382
|
const adminRouter = createRouter();
|
|
4093
4383
|
const addAdminRoute = (method, path, handler) => addRoute(adminRouter, method, path, handler);
|
|
4094
4384
|
addAdminRoute("GET", "/releases/:id", "getRelease");
|
|
@@ -4108,7 +4398,7 @@ function createHotUpdaterHandlers(api, analytics, apiKeyAuth, downloadStorageObj
|
|
|
4108
4398
|
addAdminRoute("POST", "/bundles", "createBundles");
|
|
4109
4399
|
addAdminRoute("PATCH", "/bundles/:id", "updateBundle");
|
|
4110
4400
|
addAdminRoute("DELETE", "/bundles/:id", "deleteBundle");
|
|
4111
|
-
if (
|
|
4401
|
+
if (insights !== void 0) registerInsightsAdminRoutes(addAdminRoute);
|
|
4112
4402
|
return Object.freeze({
|
|
4113
4403
|
client: createRequestHandler({
|
|
4114
4404
|
api,
|
|
@@ -4125,461 +4415,329 @@ function createHotUpdaterHandlers(api, analytics, apiKeyAuth, downloadStorageObj
|
|
|
4125
4415
|
});
|
|
4126
4416
|
}
|
|
4127
4417
|
//#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
|
-
}
|
|
4418
|
+
//#region ../../packages/server/dist/insights/provider.mjs
|
|
4419
|
+
const DEFAULT_PAGE_LIMIT = 50;
|
|
4420
|
+
const MAX_PAGE_LIMIT = 100;
|
|
4421
|
+
const MAX_EVENT_ID_LENGTH = 1024;
|
|
4422
|
+
const MAX_IDENTITY_LENGTH = 255;
|
|
4423
|
+
const MAX_CURSOR_LENGTH = 8192;
|
|
4424
|
+
const WINDOW_MS = {
|
|
4425
|
+
"24h": 864e5,
|
|
4426
|
+
"7d": 6048e5,
|
|
4427
|
+
"30d": 2592e6
|
|
4150
4428
|
};
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
}
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
const
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4429
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4430
|
+
const requireString = (value, label, maximumLength) => {
|
|
4431
|
+
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}.`);
|
|
4432
|
+
return value;
|
|
4433
|
+
};
|
|
4434
|
+
const requireTimestamp = (value, label) => {
|
|
4435
|
+
if (!Number.isSafeInteger(value) || Number(value) < 0) throw new InsightsBadRequestError(`Invalid ${label}.`);
|
|
4436
|
+
return Number(value);
|
|
4437
|
+
};
|
|
4438
|
+
const readLimit = (value) => {
|
|
4439
|
+
const limit = value ?? DEFAULT_PAGE_LIMIT;
|
|
4440
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_PAGE_LIMIT) throw new InsightsBadRequestError("Invalid page limit.");
|
|
4441
|
+
return limit;
|
|
4442
|
+
};
|
|
4443
|
+
const encodeBase64Url = (value) => {
|
|
4444
|
+
const bytes = new TextEncoder().encode(value);
|
|
4445
|
+
let binary = "";
|
|
4446
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
4447
|
+
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
|
|
4448
|
+
};
|
|
4449
|
+
const decodeBase64Url = (value) => {
|
|
4450
|
+
if (value.length === 0 || value.length > MAX_CURSOR_LENGTH) throw new InsightsBadRequestError("Invalid Insights cursor.");
|
|
4451
|
+
try {
|
|
4452
|
+
const base64 = value.replaceAll("-", "+").replaceAll("_", "/");
|
|
4453
|
+
const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
|
|
4454
|
+
const binary = atob(padded);
|
|
4455
|
+
const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
4456
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
4457
|
+
} catch {
|
|
4458
|
+
throw new InsightsBadRequestError("Invalid Insights cursor.");
|
|
4170
4459
|
}
|
|
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);
|
|
4460
|
+
};
|
|
4461
|
+
const encodeCursor = (value) => encodeBase64Url(JSON.stringify(value));
|
|
4462
|
+
const decodeCursor = (value) => {
|
|
4463
|
+
try {
|
|
4464
|
+
return JSON.parse(decodeBase64Url(value));
|
|
4465
|
+
} catch (error) {
|
|
4466
|
+
if (error instanceof InsightsBadRequestError) throw error;
|
|
4467
|
+
throw new InsightsBadRequestError("Invalid Insights cursor.");
|
|
4182
4468
|
}
|
|
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);
|
|
4469
|
+
};
|
|
4470
|
+
const readScope = (input) => {
|
|
4471
|
+
if (input.platform !== "ios" && input.platform !== "android") throw new InsightsBadRequestError("Invalid Insights platform.");
|
|
4190
4472
|
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
|
|
4473
|
+
platform: input.platform,
|
|
4474
|
+
channel: requireString(input.channel, "channel", MAX_EVENT_ID_LENGTH)
|
|
4230
4475
|
};
|
|
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
|
|
4476
|
+
};
|
|
4477
|
+
const bundleFilter = (input) => {
|
|
4478
|
+
const scope = readScope(input);
|
|
4479
|
+
const bundleId = requireString(input.bundleId, "bundle ID", MAX_EVENT_ID_LENGTH);
|
|
4480
|
+
switch (input.outcome) {
|
|
4481
|
+
case "downloaded": return {
|
|
4482
|
+
...scope,
|
|
4483
|
+
type: "UPDATE_DOWNLOADED",
|
|
4484
|
+
toBundleId: bundleId
|
|
4243
4485
|
};
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
id: last.id
|
|
4486
|
+
case "applied": return {
|
|
4487
|
+
...scope,
|
|
4488
|
+
type: "UPDATE_APPLIED",
|
|
4489
|
+
toBundleId: bundleId
|
|
4490
|
+
};
|
|
4491
|
+
case "recovered": return {
|
|
4492
|
+
...scope,
|
|
4493
|
+
type: "RECOVERED",
|
|
4494
|
+
fromBundleId: bundleId
|
|
4254
4495
|
};
|
|
4255
|
-
|
|
4496
|
+
case "unchanged": return {
|
|
4497
|
+
...scope,
|
|
4498
|
+
type: "UNCHANGED",
|
|
4499
|
+
toBundleId: bundleId
|
|
4500
|
+
};
|
|
4501
|
+
default: throw new InsightsBadRequestError("Invalid Insights outcome.");
|
|
4256
4502
|
}
|
|
4257
|
-
return rows;
|
|
4258
4503
|
};
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4504
|
+
const sameFilter = (left, right) => {
|
|
4505
|
+
if (!isRecord(left) || left.kind !== right.kind) return false;
|
|
4506
|
+
switch (right.kind) {
|
|
4507
|
+
case "all": return true;
|
|
4508
|
+
case "installationMovement": return left.installId === right.installId;
|
|
4509
|
+
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
4510
|
}
|
|
4264
4511
|
};
|
|
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;
|
|
4512
|
+
const readEventCursor = (value, filter) => {
|
|
4513
|
+
const cursor = decodeCursor(value);
|
|
4514
|
+
if (!isRecord(cursor) || cursor.version !== 2 || cursor.kind !== "events" || !isRecord(cursor.filter) || !isRecord(cursor.after)) throw new InsightsBadRequestError("Invalid Insights cursor.");
|
|
4515
|
+
if (!sameFilter(cursor.filter, filter)) throw new InsightsBadRequestError("Insights cursor does not match the requested events.");
|
|
4516
|
+
if (typeof cursor.after.id !== "string" || !isUUIDv7(cursor.after.id)) throw new InsightsBadRequestError("Invalid Insights event cursor ID.");
|
|
4287
4517
|
return {
|
|
4288
|
-
|
|
4289
|
-
|
|
4518
|
+
after: {
|
|
4519
|
+
id: cursor.after.id,
|
|
4520
|
+
receivedAtMs: requireTimestamp(cursor.after.receivedAtMs, "event cursor")
|
|
4521
|
+
},
|
|
4522
|
+
beforeReceivedAtMs: requireTimestamp(cursor.beforeReceivedAtMs, "event cutoff"),
|
|
4523
|
+
kind: "events",
|
|
4524
|
+
sinceMs: requireTimestamp(cursor.sinceMs, "event start"),
|
|
4525
|
+
filter,
|
|
4526
|
+
version: 2
|
|
4290
4527
|
};
|
|
4291
4528
|
};
|
|
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
|
-
}
|
|
4529
|
+
const readUserInstallationCursor = (value, userId) => {
|
|
4530
|
+
const cursor = decodeCursor(value);
|
|
4531
|
+
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
4532
|
return {
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
})),
|
|
4340
|
-
series: createSeries({
|
|
4341
|
-
...request,
|
|
4342
|
-
rows
|
|
4343
|
-
})
|
|
4533
|
+
afterInstallId: requireString(cursor.afterInstallId, "installation cursor", MAX_IDENTITY_LENGTH),
|
|
4534
|
+
kind: "user-installations",
|
|
4535
|
+
userId,
|
|
4536
|
+
version: 1
|
|
4344
4537
|
};
|
|
4345
4538
|
};
|
|
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);
|
|
4374
|
-
}
|
|
4375
|
-
const matchingRows = [...latestByInstall.values()].sort((left, right) => compareCodePoints(left.install_id, right.install_id));
|
|
4376
|
-
const pageSize = Math.min(Math.max(request.limit, 0), 100);
|
|
4377
|
-
return {
|
|
4378
|
-
data: matchingRows.slice(request.offset, request.offset + pageSize).map(toSearchRow),
|
|
4379
|
-
pagination: {
|
|
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
|
-
};
|
|
4539
|
+
const compareEventNewest = (left, right) => right.received_at_ms - left.received_at_ms || compareInsightsText(right.id, left.id);
|
|
4540
|
+
const isAfterEventCursor = (row, after) => row.received_at_ms < after.receivedAtMs || row.received_at_ms === after.receivedAtMs && compareInsightsText(row.id, after.id) < 0;
|
|
4541
|
+
const assertEventRows = (rows, input) => {
|
|
4542
|
+
if (rows.length > input.limit) throw new Error("Insights database returned too many event rows.");
|
|
4543
|
+
for (let index = 0; index < rows.length; index += 1) {
|
|
4544
|
+
const row = rows[index];
|
|
4545
|
+
const previous = rows[index - 1];
|
|
4546
|
+
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.");
|
|
4420
4547
|
}
|
|
4421
|
-
}
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
id: row.id,
|
|
4427
|
-
type: row.type,
|
|
4548
|
+
};
|
|
4549
|
+
const toEventHistoryRow = (row) => ({
|
|
4550
|
+
appVersion: row.app_version,
|
|
4551
|
+
channel: row.channel,
|
|
4552
|
+
cohort: row.metadata.cohort,
|
|
4428
4553
|
fromBundleId: row.from_bundle_id,
|
|
4554
|
+
id: row.id,
|
|
4555
|
+
installId: row.install_id,
|
|
4556
|
+
platform: row.platform,
|
|
4557
|
+
receivedAtMs: row.received_at_ms,
|
|
4429
4558
|
toBundleId: row.to_bundle_id,
|
|
4430
|
-
|
|
4559
|
+
type: row.type,
|
|
4431
4560
|
userId: row.user_id,
|
|
4432
|
-
|
|
4561
|
+
username: row.metadata.username
|
|
4562
|
+
});
|
|
4563
|
+
const toInstallationRow = (row) => ({
|
|
4433
4564
|
appVersion: row.app_version,
|
|
4434
4565
|
channel: row.channel,
|
|
4435
|
-
cohort: row.cohort,
|
|
4436
|
-
|
|
4566
|
+
cohort: row.metadata.cohort,
|
|
4567
|
+
installId: row.install_id,
|
|
4568
|
+
lastKnownBundleId: row.type === "UPDATE_DOWNLOADED" ? row.from_bundle_id : row.to_bundle_id,
|
|
4569
|
+
pendingBundleId: row.type === "UPDATE_DOWNLOADED" ? row.to_bundle_id : null,
|
|
4570
|
+
pendingReleaseId: row.type === "UPDATE_DOWNLOADED" ? row.to_release_id : null,
|
|
4571
|
+
latestStatus: row.type,
|
|
4572
|
+
platform: row.platform,
|
|
4573
|
+
receivedAtMs: row.received_at_ms,
|
|
4574
|
+
userId: row.user_id,
|
|
4575
|
+
username: row.metadata.username
|
|
4437
4576
|
});
|
|
4438
|
-
const
|
|
4439
|
-
const
|
|
4440
|
-
const
|
|
4441
|
-
const
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4577
|
+
const pageEventRows = async (model, input, filter, map) => {
|
|
4578
|
+
const limit = readLimit(input.limit);
|
|
4579
|
+
const cursor = input.cursor === void 0 ? void 0 : readEventCursor(input.cursor, filter);
|
|
4580
|
+
const beforeReceivedAtMs = cursor?.beforeReceivedAtMs ?? (input.beforeReceivedAtMs === void 0 ? Date.now() : requireTimestamp(input.beforeReceivedAtMs, "event cutoff"));
|
|
4581
|
+
if (cursor !== void 0 && input.beforeReceivedAtMs !== void 0 && input.beforeReceivedAtMs !== beforeReceivedAtMs) throw new InsightsBadRequestError("Insights cursor does not match the requested event cutoff.");
|
|
4582
|
+
const sinceMs = cursor?.sinceMs ?? (input.sinceMs === void 0 ? 0 : requireTimestamp(input.sinceMs, "event start"));
|
|
4583
|
+
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.");
|
|
4584
|
+
const databaseInput = {
|
|
4585
|
+
filter,
|
|
4586
|
+
sinceMs,
|
|
4587
|
+
beforeReceivedAtMs,
|
|
4588
|
+
...cursor === void 0 ? {} : { after: cursor.after },
|
|
4589
|
+
limit: limit + 1
|
|
4445
4590
|
};
|
|
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);
|
|
4591
|
+
const rows = await model.listEvents(databaseInput);
|
|
4592
|
+
assertEventRows(rows, databaseInput);
|
|
4593
|
+
const pageRows = rows.slice(0, limit);
|
|
4594
|
+
const last = pageRows.at(-1);
|
|
4460
4595
|
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
|
-
}
|
|
4596
|
+
beforeReceivedAtMs,
|
|
4597
|
+
data: pageRows.map(map),
|
|
4598
|
+
nextCursor: rows.length > limit && last ? encodeCursor({
|
|
4599
|
+
after: {
|
|
4600
|
+
id: last.id,
|
|
4601
|
+
receivedAtMs: last.received_at_ms
|
|
4602
|
+
},
|
|
4603
|
+
beforeReceivedAtMs,
|
|
4604
|
+
kind: "events",
|
|
4605
|
+
filter,
|
|
4606
|
+
sinceMs,
|
|
4607
|
+
version: 2
|
|
4608
|
+
}) : null
|
|
4481
4609
|
};
|
|
4482
4610
|
};
|
|
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);
|
|
4611
|
+
const assertInstallationRows = (rows, input) => {
|
|
4612
|
+
if (rows.length > input.limit) throw new Error("Insights database returned too many installation rows.");
|
|
4613
|
+
for (let index = 0; index < rows.length; index += 1) {
|
|
4614
|
+
const row = rows[index];
|
|
4615
|
+
const previous = rows[index - 1];
|
|
4616
|
+
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
4617
|
}
|
|
4501
|
-
return normalizedBundleIds.map((bundleId) => ({
|
|
4502
|
-
bundleId,
|
|
4503
|
-
installed: installedByBundleId.get(bundleId)?.size ?? 0,
|
|
4504
|
-
recovered: recoveredByBundleId.get(bundleId)?.size ?? 0
|
|
4505
|
-
}));
|
|
4506
4618
|
};
|
|
4507
|
-
const
|
|
4508
|
-
mode: "bounded",
|
|
4509
|
-
maxMatchingRows: ANALYTICS_SCAN_MAX_ROWS,
|
|
4619
|
+
const createInsightsProvider = (model) => Object.freeze({
|
|
4510
4620
|
async appendBundleEvent(input) {
|
|
4511
|
-
|
|
4621
|
+
const event = createBundleEventRow(input);
|
|
4622
|
+
await model.recordEvent({ event });
|
|
4512
4623
|
},
|
|
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)))
|
|
4624
|
+
listEvents(input) {
|
|
4625
|
+
const filter = input.bundle === void 0 ? { kind: "all" } : {
|
|
4626
|
+
kind: "bundle",
|
|
4627
|
+
...bundleFilter(input.bundle)
|
|
4521
4628
|
};
|
|
4629
|
+
return pageEventRows(model, input, filter, toEventHistoryRow);
|
|
4522
4630
|
},
|
|
4523
|
-
|
|
4524
|
-
|
|
4631
|
+
async listInstallationEvents(input) {
|
|
4632
|
+
if ("bundle" in input && input.bundle !== void 0) throw new InsightsBadRequestError("Installation movement queries cannot include a bundle filter.");
|
|
4633
|
+
const installId = requireString(input.installId, "install ID", MAX_IDENTITY_LENGTH);
|
|
4634
|
+
return pageEventRows(model, input, {
|
|
4635
|
+
kind: "installationMovement",
|
|
4636
|
+
installId
|
|
4637
|
+
}, (row) => toEventHistoryRow(row));
|
|
4525
4638
|
},
|
|
4526
|
-
|
|
4527
|
-
|
|
4639
|
+
async getInstallation({ installId }) {
|
|
4640
|
+
const normalizedInstallId = requireString(installId, "install ID", MAX_IDENTITY_LENGTH);
|
|
4641
|
+
const rows = await model.findLatestEvents({ installId: normalizedInstallId });
|
|
4642
|
+
const row = rows[0] ?? null;
|
|
4643
|
+
if (rows.length > 1 || row !== null && row.install_id !== normalizedInstallId) throw new Error("Insights database returned an invalid installation.");
|
|
4644
|
+
return row === null ? null : toInstallationRow(row);
|
|
4528
4645
|
},
|
|
4529
|
-
async
|
|
4530
|
-
const
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4646
|
+
async pageInstallationsByCurrentUserId(input) {
|
|
4647
|
+
const userId = requireString(input.userId, "user ID", MAX_IDENTITY_LENGTH);
|
|
4648
|
+
const limit = readLimit(input.limit);
|
|
4649
|
+
const cursor = input.cursor === void 0 ? void 0 : readUserInstallationCursor(input.cursor, userId);
|
|
4650
|
+
const databaseInput = {
|
|
4651
|
+
userId,
|
|
4652
|
+
...cursor === void 0 ? {} : { afterInstallId: cursor.afterInstallId },
|
|
4653
|
+
limit: limit + 1
|
|
4654
|
+
};
|
|
4655
|
+
const rows = await model.findLatestEvents(databaseInput);
|
|
4656
|
+
assertInstallationRows(rows, databaseInput);
|
|
4657
|
+
const pageRows = rows.slice(0, limit);
|
|
4658
|
+
const last = pageRows.at(-1);
|
|
4541
4659
|
return {
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
installations
|
|
4546
|
-
|
|
4660
|
+
data: pageRows.map(toInstallationRow),
|
|
4661
|
+
nextCursor: rows.length > limit && last ? encodeCursor({
|
|
4662
|
+
afterInstallId: last.install_id,
|
|
4663
|
+
kind: "user-installations",
|
|
4664
|
+
userId,
|
|
4665
|
+
version: 1
|
|
4666
|
+
}) : null
|
|
4547
4667
|
};
|
|
4548
4668
|
},
|
|
4549
|
-
async
|
|
4550
|
-
const
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4669
|
+
async getReportingOverview(input) {
|
|
4670
|
+
const scope = readScope(input);
|
|
4671
|
+
const { window } = input;
|
|
4672
|
+
if (!Object.hasOwn(WINDOW_MS, window)) throw new InsightsBadRequestError("Invalid reporting installation window.");
|
|
4673
|
+
const beforeReceivedAtMs = Date.now();
|
|
4674
|
+
const sinceMs = Math.max(0, beforeReceivedAtMs - WINDOW_MS[window]);
|
|
4675
|
+
const measure = async (count) => {
|
|
4676
|
+
const value = await count;
|
|
4677
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new Error("Insights database returned an invalid count.");
|
|
4678
|
+
return {
|
|
4679
|
+
count: value,
|
|
4680
|
+
measuredAtMs: Date.now()
|
|
4681
|
+
};
|
|
4682
|
+
};
|
|
4683
|
+
const bundleId = input.bundleId === void 0 ? void 0 : requireString(input.bundleId, "bundle ID", MAX_EVENT_ID_LENGTH);
|
|
4684
|
+
const reporting = measure(model.countLatestEvents({
|
|
4685
|
+
...scope,
|
|
4686
|
+
sinceMs
|
|
4687
|
+
}));
|
|
4688
|
+
if (bundleId === void 0) return {
|
|
4689
|
+
...scope,
|
|
4690
|
+
window,
|
|
4691
|
+
sinceMs,
|
|
4692
|
+
beforeReceivedAtMs,
|
|
4693
|
+
reportingInstallations: await reporting
|
|
4694
|
+
};
|
|
4695
|
+
const countOutcome = (outcome) => measure(model.countEvents({
|
|
4696
|
+
filter: bundleFilter({
|
|
4697
|
+
...scope,
|
|
4698
|
+
bundleId,
|
|
4699
|
+
outcome
|
|
4566
4700
|
}),
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4701
|
+
sinceMs,
|
|
4702
|
+
beforeReceivedAtMs
|
|
4703
|
+
}));
|
|
4704
|
+
const [reportingInstallations, bundleInstallations, downloadedReports, appliedReports, recoveredReports, unchangedReports] = await Promise.all([
|
|
4705
|
+
reporting,
|
|
4706
|
+
measure(model.countLatestEvents({
|
|
4707
|
+
...scope,
|
|
4708
|
+
sinceMs,
|
|
4709
|
+
bundle: [{
|
|
4710
|
+
field: "from_bundle_id",
|
|
4711
|
+
value: bundleId,
|
|
4712
|
+
types: ["UPDATE_DOWNLOADED"]
|
|
4713
|
+
}, {
|
|
4714
|
+
field: "to_bundle_id",
|
|
4715
|
+
value: bundleId,
|
|
4716
|
+
types: [
|
|
4717
|
+
"UNCHANGED",
|
|
4718
|
+
"UPDATE_APPLIED",
|
|
4719
|
+
"RECOVERED"
|
|
4720
|
+
]
|
|
4721
|
+
}]
|
|
4722
|
+
})),
|
|
4723
|
+
countOutcome("downloaded"),
|
|
4724
|
+
countOutcome("applied"),
|
|
4725
|
+
countOutcome("recovered"),
|
|
4726
|
+
countOutcome("unchanged")
|
|
4727
|
+
]);
|
|
4577
4728
|
return {
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4729
|
+
...scope,
|
|
4730
|
+
window,
|
|
4731
|
+
sinceMs,
|
|
4732
|
+
beforeReceivedAtMs,
|
|
4733
|
+
reportingInstallations,
|
|
4734
|
+
bundle: {
|
|
4735
|
+
bundleId,
|
|
4736
|
+
reportingInstallations: bundleInstallations,
|
|
4737
|
+
downloadedReports,
|
|
4738
|
+
appliedReports,
|
|
4739
|
+
recoveredReports,
|
|
4740
|
+
unchangedReports
|
|
4583
4741
|
}
|
|
4584
4742
|
};
|
|
4585
4743
|
}
|
|
@@ -4620,7 +4778,7 @@ const hashApiKey = async (apiKey) => {
|
|
|
4620
4778
|
return bytesToBase64Url(new Uint8Array(digest));
|
|
4621
4779
|
};
|
|
4622
4780
|
const apiKeyId = () => {
|
|
4623
|
-
const bytes = new Uint8Array(16);
|
|
4781
|
+
const bytes = /* @__PURE__ */ new Uint8Array(16);
|
|
4624
4782
|
crypto.getRandomValues(bytes);
|
|
4625
4783
|
return `api-${bytesToBase64Url(bytes)}`;
|
|
4626
4784
|
};
|
|
@@ -4657,7 +4815,7 @@ const registerApiKey = async (input) => {
|
|
|
4657
4815
|
});
|
|
4658
4816
|
};
|
|
4659
4817
|
const createApiKey = (input) => {
|
|
4660
|
-
const bytes = new Uint8Array(32);
|
|
4818
|
+
const bytes = /* @__PURE__ */ new Uint8Array(32);
|
|
4661
4819
|
crypto.getRandomValues(bytes);
|
|
4662
4820
|
return registerApiKey({
|
|
4663
4821
|
apiKey: bytesToBase64Url(bytes),
|
|
@@ -5077,6 +5235,8 @@ const unsupportedSchemaUpgradeMessage = (version) => `Hot Updater v1 cannot migr
|
|
|
5077
5235
|
//#endregion
|
|
5078
5236
|
//#region ../../packages/server/dist/db/schemaReadiness.mjs
|
|
5079
5237
|
var HotUpdaterSchemaMigrationRequiredError = class extends Error {
|
|
5238
|
+
adapterName;
|
|
5239
|
+
currentVersion;
|
|
5080
5240
|
constructor(adapterName, currentVersion) {
|
|
5081
5241
|
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
5242
|
this.adapterName = adapterName;
|
|
@@ -5106,7 +5266,7 @@ const sqlProviders = [
|
|
|
5106
5266
|
const noSqlProviders = ["mongodb"];
|
|
5107
5267
|
[...sqlProviders, ...noSqlProviders];
|
|
5108
5268
|
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" && "
|
|
5269
|
+
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 && "recordEvent" in plugin.models.insights && typeof plugin.models.insights.recordEvent === "function" && "listEvents" in plugin.models.insights && typeof plugin.models.insights.listEvents === "function" && "findLatestEvents" in plugin.models.insights && typeof plugin.models.insights.findLatestEvents === "function" && "countEvents" in plugin.models.insights && typeof plugin.models.insights.countEvents === "function" && "countLatestEvents" in plugin.models.insights && typeof plugin.models.insights.countLatestEvents === "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
5270
|
}
|
|
5111
5271
|
//#endregion
|
|
5112
5272
|
//#region ../../packages/server/dist/storageAccess.mjs
|
|
@@ -5209,34 +5369,48 @@ const hotUpdaterCoreMetadata = Symbol.for("@hot-updater/server/core-metadata");
|
|
|
5209
5369
|
function createHotUpdaterCore(options) {
|
|
5210
5370
|
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
5371
|
const database = options.database;
|
|
5212
|
-
const
|
|
5372
|
+
const storagePlugins = (options.storage ?? []).map((storage) => {
|
|
5213
5373
|
assertStorageOperations(storage, ["get", "getDownloadUrl"]);
|
|
5214
5374
|
return storage;
|
|
5215
|
-
})
|
|
5375
|
+
});
|
|
5376
|
+
const { downloadStorageObject, readStorageText, resolveFileUrl } = createStorageAccess(storagePlugins);
|
|
5216
5377
|
const adapterCapabilities = database;
|
|
5217
5378
|
if (!isDatabasePlugin(database)) throw new Error("@hot-updater/server only supports database plugins.");
|
|
5218
5379
|
const plugin = database;
|
|
5219
|
-
const
|
|
5380
|
+
const adapterName = adapterCapabilities.adapterName ?? plugin.name;
|
|
5381
|
+
const assertSchemaReady = createSchemaReadinessChecker(adapterName, adapterCapabilities.createMigrator);
|
|
5220
5382
|
const core = createDatabasePluginCore(plugin, resolveFileUrl, {
|
|
5221
5383
|
beforeOperation: assertSchemaReady,
|
|
5222
5384
|
readStorageText
|
|
5223
5385
|
});
|
|
5224
5386
|
const clientAccess = normalizeClientAccess(options.clientAccess);
|
|
5225
|
-
const
|
|
5226
|
-
async
|
|
5387
|
+
const insights = createInsightsProvider({
|
|
5388
|
+
async recordEvent(input) {
|
|
5389
|
+
await assertSchemaReady();
|
|
5390
|
+
return plugin.models.insights.recordEvent(input);
|
|
5391
|
+
},
|
|
5392
|
+
async listEvents(input) {
|
|
5393
|
+
await assertSchemaReady();
|
|
5394
|
+
return plugin.models.insights.listEvents(input);
|
|
5395
|
+
},
|
|
5396
|
+
async findLatestEvents(input) {
|
|
5227
5397
|
await assertSchemaReady();
|
|
5228
|
-
return plugin.models.
|
|
5398
|
+
return plugin.models.insights.findLatestEvents(input);
|
|
5229
5399
|
},
|
|
5230
|
-
async
|
|
5400
|
+
async countLatestEvents(input) {
|
|
5231
5401
|
await assertSchemaReady();
|
|
5232
|
-
return plugin.models.
|
|
5402
|
+
return plugin.models.insights.countLatestEvents(input);
|
|
5403
|
+
},
|
|
5404
|
+
async countEvents(input) {
|
|
5405
|
+
await assertSchemaReady();
|
|
5406
|
+
return plugin.models.insights.countEvents(input);
|
|
5233
5407
|
}
|
|
5234
5408
|
});
|
|
5235
5409
|
const apiKeys = createApiKeyManagement({
|
|
5236
5410
|
apiKeys: plugin.models.apiKeys,
|
|
5237
5411
|
beforeOperation: assertSchemaReady
|
|
5238
5412
|
});
|
|
5239
|
-
const handlers = createHotUpdaterHandlers(core.api,
|
|
5413
|
+
const handlers = createHotUpdaterHandlers(core.api, insights, clientAccess.type === "api-key" ? {
|
|
5240
5414
|
authenticate: (request) => authenticateApiKey({
|
|
5241
5415
|
apiKeys: plugin.models.apiKeys,
|
|
5242
5416
|
beforeLookup: assertSchemaReady,
|
|
@@ -5247,7 +5421,7 @@ function createHotUpdaterCore(options) {
|
|
|
5247
5421
|
} : void 0, downloadStorageObject);
|
|
5248
5422
|
const api = Object.assign({
|
|
5249
5423
|
adapterName: adapterCapabilities.adapterName ?? core.adapterName,
|
|
5250
|
-
|
|
5424
|
+
insights,
|
|
5251
5425
|
apiKeys,
|
|
5252
5426
|
handlers
|
|
5253
5427
|
}, core.api);
|
|
@@ -5268,8 +5442,60 @@ function createHotUpdater(options) {
|
|
|
5268
5442
|
return createHotUpdaterCore(options).api;
|
|
5269
5443
|
}
|
|
5270
5444
|
//#endregion
|
|
5445
|
+
//#region ../plugin-core/dist/insightsLatestQueries.mjs
|
|
5446
|
+
/** Internal query translation for the bundled CRUD adapters. */
|
|
5447
|
+
const latestInsightsWhere = (input) => {
|
|
5448
|
+
if ("installId" in input) return [{
|
|
5449
|
+
field: "install_id",
|
|
5450
|
+
value: input.installId
|
|
5451
|
+
}];
|
|
5452
|
+
if ("userId" in input) return [{
|
|
5453
|
+
field: "user_id",
|
|
5454
|
+
value: input.userId
|
|
5455
|
+
}, ...input.afterInstallId === void 0 ? [] : [{
|
|
5456
|
+
field: "install_id",
|
|
5457
|
+
operator: "gt",
|
|
5458
|
+
value: input.afterInstallId
|
|
5459
|
+
}]];
|
|
5460
|
+
return [
|
|
5461
|
+
{
|
|
5462
|
+
field: "platform",
|
|
5463
|
+
value: input.platform
|
|
5464
|
+
},
|
|
5465
|
+
{
|
|
5466
|
+
field: "channel",
|
|
5467
|
+
value: input.channel
|
|
5468
|
+
},
|
|
5469
|
+
{
|
|
5470
|
+
field: "received_at_ms",
|
|
5471
|
+
operator: "gte",
|
|
5472
|
+
value: input.sinceMs
|
|
5473
|
+
},
|
|
5474
|
+
...input.bundle === void 0 ? [] : [input.bundle.field === "from_bundle_id" ? {
|
|
5475
|
+
field: "from_bundle_id",
|
|
5476
|
+
value: input.bundle.value
|
|
5477
|
+
} : {
|
|
5478
|
+
field: "to_bundle_id",
|
|
5479
|
+
value: input.bundle.value
|
|
5480
|
+
}, {
|
|
5481
|
+
field: "type",
|
|
5482
|
+
operator: "in",
|
|
5483
|
+
value: input.bundle.types
|
|
5484
|
+
}]
|
|
5485
|
+
];
|
|
5486
|
+
};
|
|
5487
|
+
/** OR groups for one latest-event count; an installation is counted once. */
|
|
5488
|
+
const latestInsightsCountGroups = (input) => input.bundle === void 0 ? [latestInsightsWhere({
|
|
5489
|
+
...input,
|
|
5490
|
+
bundle: void 0
|
|
5491
|
+
})] : input.bundle.map((bundle) => latestInsightsWhere({
|
|
5492
|
+
...input,
|
|
5493
|
+
bundle
|
|
5494
|
+
}));
|
|
5495
|
+
//#endregion
|
|
5271
5496
|
//#region src/firebaseDatabaseParserShared.ts
|
|
5272
5497
|
var FirebaseDatabaseDataError = class extends Error {
|
|
5498
|
+
source;
|
|
5273
5499
|
name = "FirebaseDatabaseDataError";
|
|
5274
5500
|
constructor(source) {
|
|
5275
5501
|
super(`Invalid Firebase database data at "${source}".`);
|
|
@@ -5359,14 +5585,15 @@ const parseFirebaseBundleEventRow = (value, source) => {
|
|
|
5359
5585
|
const input = record(value, source);
|
|
5360
5586
|
const type = string(property(input, "type"), source);
|
|
5361
5587
|
const fromBundleId = requiredNullableString(input, "from_bundle_id", source);
|
|
5362
|
-
const
|
|
5363
|
-
if (!(
|
|
5588
|
+
const metadata = property(input, "metadata");
|
|
5589
|
+
if (!isDatabaseBundleEventMetadata(metadata)) throw new FirebaseDatabaseDataError(source);
|
|
5590
|
+
const updateStrategy = metadata.update_strategy;
|
|
5591
|
+
if (!((type === "UPDATE_DOWNLOADED" || type === "UPDATE_APPLIED" || type === "RECOVERED") && typeof fromBundleId === "string" && (updateStrategy === "fingerprint" || updateStrategy === "appVersion") || type === "UNCHANGED" && fromBundleId === null && updateStrategy === null)) throw new FirebaseDatabaseDataError(source);
|
|
5364
5592
|
return {
|
|
5365
5593
|
id: string(property(input, "id"), source),
|
|
5366
5594
|
type,
|
|
5367
5595
|
install_id: string(property(input, "install_id"), source),
|
|
5368
5596
|
user_id: nullableString(property(input, "user_id"), source),
|
|
5369
|
-
username: nullableString(property(input, "username"), source),
|
|
5370
5597
|
from_release_id: requiredNullableString(input, "from_release_id", source),
|
|
5371
5598
|
from_bundle_id: fromBundleId,
|
|
5372
5599
|
to_release_id: requiredNullableString(input, "to_release_id", source),
|
|
@@ -5374,10 +5601,7 @@ const parseFirebaseBundleEventRow = (value, source) => {
|
|
|
5374
5601
|
platform: platform(property(input, "platform"), source),
|
|
5375
5602
|
app_version: string(property(input, "app_version"), source),
|
|
5376
5603
|
channel: string(property(input, "channel"), source),
|
|
5377
|
-
|
|
5378
|
-
update_strategy: updateStrategy,
|
|
5379
|
-
fingerprint_hash: nullableString(property(input, "fingerprint_hash"), source),
|
|
5380
|
-
sdk_version: nullableString(property(input, "sdk_version"), source),
|
|
5604
|
+
metadata,
|
|
5381
5605
|
received_at_ms: number(property(input, "received_at_ms"), source)
|
|
5382
5606
|
};
|
|
5383
5607
|
};
|
|
@@ -5461,13 +5685,15 @@ const matchesCondition = (row, condition) => {
|
|
|
5461
5685
|
switch (condition.operator ?? "eq") {
|
|
5462
5686
|
case "eq": {
|
|
5463
5687
|
if (typeof expected !== "string") return actual === expected;
|
|
5464
|
-
const
|
|
5688
|
+
const mode = "mode" in condition ? condition.mode : void 0;
|
|
5689
|
+
const comparison = normalizeStringComparison(actual, expected, mode);
|
|
5465
5690
|
return comparison !== null && comparison[0] === comparison[1];
|
|
5466
5691
|
}
|
|
5467
5692
|
case "ne": {
|
|
5468
5693
|
if (actual === null || actual === void 0) return false;
|
|
5469
5694
|
if (typeof expected !== "string") return actual !== expected;
|
|
5470
|
-
const
|
|
5695
|
+
const mode = "mode" in condition ? condition.mode : void 0;
|
|
5696
|
+
const comparison = normalizeStringComparison(actual, expected, mode);
|
|
5471
5697
|
return comparison === null || comparison[0] !== comparison[1];
|
|
5472
5698
|
}
|
|
5473
5699
|
case "gt":
|
|
@@ -5492,17 +5718,20 @@ const matchesCondition = (row, condition) => {
|
|
|
5492
5718
|
}
|
|
5493
5719
|
case "contains": {
|
|
5494
5720
|
if (typeof expected !== "string") return false;
|
|
5495
|
-
const
|
|
5721
|
+
const mode = "mode" in condition ? condition.mode : void 0;
|
|
5722
|
+
const comparison = normalizeStringComparison(actual, expected, mode);
|
|
5496
5723
|
return comparison?.[0].includes(comparison[1]) ?? false;
|
|
5497
5724
|
}
|
|
5498
5725
|
case "starts_with": {
|
|
5499
5726
|
if (typeof expected !== "string") return false;
|
|
5500
|
-
const
|
|
5727
|
+
const mode = "mode" in condition ? condition.mode : void 0;
|
|
5728
|
+
const comparison = normalizeStringComparison(actual, expected, mode);
|
|
5501
5729
|
return comparison?.[0].startsWith(comparison[1]) ?? false;
|
|
5502
5730
|
}
|
|
5503
5731
|
case "ends_with": {
|
|
5504
5732
|
if (typeof expected !== "string") return false;
|
|
5505
|
-
const
|
|
5733
|
+
const mode = "mode" in condition ? condition.mode : void 0;
|
|
5734
|
+
const comparison = normalizeStringComparison(actual, expected, mode);
|
|
5506
5735
|
return comparison?.[0].endsWith(comparison[1]) ?? false;
|
|
5507
5736
|
}
|
|
5508
5737
|
}
|
|
@@ -5547,6 +5776,7 @@ const queryFirebaseDatabaseRows = (rows, input) => {
|
|
|
5547
5776
|
//#endregion
|
|
5548
5777
|
//#region src/firebaseDatabaseState.ts
|
|
5549
5778
|
var FirebaseDatabaseConstraintError = class extends Error {
|
|
5779
|
+
constraint;
|
|
5550
5780
|
name = "FirebaseDatabaseConstraintError";
|
|
5551
5781
|
constructor(constraint) {
|
|
5552
5782
|
super(`Firebase database constraint failed: ${constraint}`);
|
|
@@ -5676,6 +5906,7 @@ const createFirebaseDatabaseState = (snapshot) => ({
|
|
|
5676
5906
|
case "bundles": return distinctCount([...snapshot.bundles.values()].filter((row) => matchesFirebaseDatabaseWhere(row, input.where)), input.distinct);
|
|
5677
5907
|
case "bundle_patches": return distinctCount([...snapshot.bundlePatches.values()].filter((row) => matchesFirebaseDatabaseWhere(row, input.where)), input.distinct);
|
|
5678
5908
|
case "releases": return distinctCount([...snapshot.releases.values()].filter((row) => matchesFirebaseDatabaseWhere(row, input.where)), input.distinct);
|
|
5909
|
+
case "bundle_events": return distinctCount([...snapshot.bundleEvents.values()].filter((row) => matchesFirebaseDatabaseWhere(row, input.where)), input.distinct);
|
|
5679
5910
|
}
|
|
5680
5911
|
},
|
|
5681
5912
|
async findOne(input) {
|
|
@@ -5705,6 +5936,7 @@ const createFirebaseDatabaseState = (snapshot) => ({
|
|
|
5705
5936
|
const FIREBASE_V1_COLLECTION_NAMES = {
|
|
5706
5937
|
apiKeys: "hot_updater_v1_api_keys",
|
|
5707
5938
|
bundleEvents: "hot_updater_v1_bundle_events",
|
|
5939
|
+
insightsLatest: "hot_updater_v1_insights_latest",
|
|
5708
5940
|
bundlePatches: "hot_updater_v1_bundle_patches",
|
|
5709
5941
|
bundles: "hot_updater_v1_bundles",
|
|
5710
5942
|
channels: "hot_updater_v1_channels",
|
|
@@ -5715,6 +5947,7 @@ const FIREBASE_V1_COLLECTION_NAMES = {
|
|
|
5715
5947
|
//#endregion
|
|
5716
5948
|
//#region src/firebaseDatabasePersistence.ts
|
|
5717
5949
|
var FirebaseDatabaseAdapterVersionError = class extends Error {
|
|
5950
|
+
version;
|
|
5718
5951
|
name = "FirebaseDatabaseAdapterVersionError";
|
|
5719
5952
|
constructor(version) {
|
|
5720
5953
|
super(`Unsupported Firebase database adapter version: ${String(version)}`);
|
|
@@ -5725,6 +5958,7 @@ const createFirebaseDatabaseCollections = (db) => ({
|
|
|
5725
5958
|
bundles: db.collection(FIREBASE_V1_COLLECTION_NAMES.bundles),
|
|
5726
5959
|
bundlePatches: db.collection(FIREBASE_V1_COLLECTION_NAMES.bundlePatches),
|
|
5727
5960
|
bundleEvents: db.collection(FIREBASE_V1_COLLECTION_NAMES.bundleEvents),
|
|
5961
|
+
insightsLatest: db.collection(FIREBASE_V1_COLLECTION_NAMES.insightsLatest),
|
|
5728
5962
|
channels: db.collection(FIREBASE_V1_COLLECTION_NAMES.channels),
|
|
5729
5963
|
apiKeys: db.collection(FIREBASE_V1_COLLECTION_NAMES.apiKeys),
|
|
5730
5964
|
releaseCatalogs: db.collection(FIREBASE_V1_COLLECTION_NAMES.releaseCatalogs),
|
|
@@ -5733,14 +5967,15 @@ const createFirebaseDatabaseCollections = (db) => ({
|
|
|
5733
5967
|
});
|
|
5734
5968
|
const firebaseChannelDocumentId = (name) => `name_${Buffer.from(name, "utf8").toString("base64url")}`;
|
|
5735
5969
|
const firebaseChannelIdDocumentId = (id) => `channel_id_${Buffer.from(id, "utf8").toString("base64url")}`;
|
|
5970
|
+
const firebaseInstallationDocumentId = (id) => `install_${Buffer.from(id, "utf8").toString("base64url")}`;
|
|
5736
5971
|
const requireFirebaseDocumentKey = (model, documentId, row) => {
|
|
5737
|
-
if (documentId !== ("id" in row ? row.id : row.scope_key)) throw new FirebaseDatabaseConstraintError(`${model}.id.document-key`);
|
|
5972
|
+
if (documentId !== (model === "insights_latest" ? firebaseInstallationDocumentId(row.install_id) : "id" in row ? row.id : row.scope_key)) throw new FirebaseDatabaseConstraintError(`${model}.id.document-key`);
|
|
5738
5973
|
return row;
|
|
5739
5974
|
};
|
|
5740
5975
|
const documentMap = (model, documents) => {
|
|
5741
5976
|
const rows = /* @__PURE__ */ new Map();
|
|
5742
5977
|
for (const { row } of documents) {
|
|
5743
|
-
const key = "id" in row ? row.id : row.scope_key;
|
|
5978
|
+
const key = model === "insights_latest" ? row.install_id : "id" in row ? row.id : row.scope_key;
|
|
5744
5979
|
if (rows.has(key)) throw new FirebaseDatabaseConstraintError(`${model}.id.unique`);
|
|
5745
5980
|
rows.set(key, row);
|
|
5746
5981
|
}
|
|
@@ -5755,10 +5990,6 @@ const patchMap = (snapshot) => documentMap("bundle_patches", snapshot.docs.map((
|
|
|
5755
5990
|
document,
|
|
5756
5991
|
row: parseFirebasePatchRow(document.data(), `bundle_patches/${document.id}`)
|
|
5757
5992
|
})));
|
|
5758
|
-
const eventMap = (snapshot) => documentMap("bundle_events", snapshot.docs.map((document) => ({
|
|
5759
|
-
document,
|
|
5760
|
-
row: parseFirebaseBundleEventRow(document.data(), `bundle_events/${document.id}`)
|
|
5761
|
-
})));
|
|
5762
5993
|
const channelMap = (snapshot) => {
|
|
5763
5994
|
const rows = /* @__PURE__ */ new Map();
|
|
5764
5995
|
const names = /* @__PURE__ */ new Set();
|
|
@@ -5789,18 +6020,17 @@ const toSnapshot = (documents) => {
|
|
|
5789
6020
|
return {
|
|
5790
6021
|
bundles: bundleMap(documents[0]),
|
|
5791
6022
|
bundlePatches: patchMap(documents[1]),
|
|
5792
|
-
bundleEvents:
|
|
5793
|
-
channels: channelMap(documents[
|
|
5794
|
-
apiKeys: apiKeyMap(documents[
|
|
5795
|
-
releases: releaseMap(documents[
|
|
5796
|
-
releaseCatalogs: releaseCatalogMap(documents[
|
|
6023
|
+
bundleEvents: /* @__PURE__ */ new Map(),
|
|
6024
|
+
channels: channelMap(documents[2]),
|
|
6025
|
+
apiKeys: apiKeyMap(documents[3]),
|
|
6026
|
+
releases: releaseMap(documents[4]),
|
|
6027
|
+
releaseCatalogs: releaseCatalogMap(documents[5])
|
|
5797
6028
|
};
|
|
5798
6029
|
};
|
|
5799
6030
|
const loadFirebaseDatabaseSnapshot = async (collections) => {
|
|
5800
|
-
const [bundles, patches,
|
|
6031
|
+
const [bundles, patches, channels, apiKeys, releases, releaseCatalogs] = await Promise.all([
|
|
5801
6032
|
collections.bundles.get(),
|
|
5802
6033
|
collections.bundlePatches.get(),
|
|
5803
|
-
collections.bundleEvents.get(),
|
|
5804
6034
|
collections.channels.get(),
|
|
5805
6035
|
collections.apiKeys.get(),
|
|
5806
6036
|
collections.releases.get(),
|
|
@@ -5809,7 +6039,6 @@ const loadFirebaseDatabaseSnapshot = async (collections) => {
|
|
|
5809
6039
|
return toSnapshot([
|
|
5810
6040
|
bundles,
|
|
5811
6041
|
patches,
|
|
5812
|
-
events,
|
|
5813
6042
|
channels,
|
|
5814
6043
|
apiKeys,
|
|
5815
6044
|
releases,
|
|
@@ -5817,10 +6046,9 @@ const loadFirebaseDatabaseSnapshot = async (collections) => {
|
|
|
5817
6046
|
]);
|
|
5818
6047
|
};
|
|
5819
6048
|
const loadFirebaseTransactionSnapshot = async (transaction, collections) => {
|
|
5820
|
-
const [bundles, patches,
|
|
6049
|
+
const [bundles, patches, channels, apiKeys, releases, releaseCatalogs] = await Promise.all([
|
|
5821
6050
|
transaction.get(collections.bundles),
|
|
5822
6051
|
transaction.get(collections.bundlePatches),
|
|
5823
|
-
transaction.get(collections.bundleEvents),
|
|
5824
6052
|
transaction.get(collections.channels),
|
|
5825
6053
|
transaction.get(collections.apiKeys),
|
|
5826
6054
|
transaction.get(collections.releases),
|
|
@@ -5829,7 +6057,6 @@ const loadFirebaseTransactionSnapshot = async (transaction, collections) => {
|
|
|
5829
6057
|
return toSnapshot([
|
|
5830
6058
|
bundles,
|
|
5831
6059
|
patches,
|
|
5832
|
-
events,
|
|
5833
6060
|
channels,
|
|
5834
6061
|
apiKeys,
|
|
5835
6062
|
releases,
|
|
@@ -5855,13 +6082,6 @@ const persistFirebaseDatabaseSnapshot = ({ transaction, collections, before, aft
|
|
|
5855
6082
|
after: after.bundlePatches,
|
|
5856
6083
|
documentId: (row) => row.id
|
|
5857
6084
|
});
|
|
5858
|
-
persistCollection({
|
|
5859
|
-
transaction,
|
|
5860
|
-
collection: collections.bundleEvents,
|
|
5861
|
-
before: before.bundleEvents,
|
|
5862
|
-
after: after.bundleEvents,
|
|
5863
|
-
documentId: (row) => row.id
|
|
5864
|
-
});
|
|
5865
6085
|
persistCollection({
|
|
5866
6086
|
transaction,
|
|
5867
6087
|
collection: collections.channels,
|
|
@@ -5893,7 +6113,7 @@ const persistFirebaseDatabaseSnapshot = ({ transaction, collections, before, aft
|
|
|
5893
6113
|
documentId: (row) => row.scope_key
|
|
5894
6114
|
});
|
|
5895
6115
|
};
|
|
5896
|
-
const migrateFirebaseDatabase = async (
|
|
6116
|
+
const migrateFirebaseDatabase = async (collections) => {
|
|
5897
6117
|
const versionDocument = collections.settings.doc("database_adapter_version");
|
|
5898
6118
|
const version = await versionDocument.get();
|
|
5899
6119
|
const adapterVersion = version.data()?.version;
|
|
@@ -5904,7 +6124,9 @@ const migrateFirebaseDatabase = async (_db, collections) => {
|
|
|
5904
6124
|
collections.bundlePatches.limit(1).get(),
|
|
5905
6125
|
collections.channels.limit(1).get(),
|
|
5906
6126
|
collections.releases.limit(1).get(),
|
|
5907
|
-
collections.releaseCatalogs.limit(1).get()
|
|
6127
|
+
collections.releaseCatalogs.limit(1).get(),
|
|
6128
|
+
collections.insightsLatest.limit(1).get(),
|
|
6129
|
+
collections.bundleEvents.limit(1).get()
|
|
5908
6130
|
])).some((snapshot) => !snapshot.empty)) throw new FirebaseDatabaseAdapterVersionError("v0");
|
|
5909
6131
|
try {
|
|
5910
6132
|
await versionDocument.create({ version: 4 });
|
|
@@ -5919,13 +6141,36 @@ const exactId = (input) => {
|
|
|
5919
6141
|
const [condition] = input.where;
|
|
5920
6142
|
return condition.field === "id" && (condition.operator === void 0 || condition.operator === "eq") && typeof condition.value === "string" ? condition.value : void 0;
|
|
5921
6143
|
};
|
|
6144
|
+
const firestoreOperator = (operator) => {
|
|
6145
|
+
switch (operator ?? "eq") {
|
|
6146
|
+
case "eq": return "==";
|
|
6147
|
+
case "ne": return "!=";
|
|
6148
|
+
case "gt": return ">";
|
|
6149
|
+
case "gte": return ">=";
|
|
6150
|
+
case "lt": return "<";
|
|
6151
|
+
case "lte": return "<=";
|
|
6152
|
+
case "in": return "in";
|
|
6153
|
+
case "not_in": return "not-in";
|
|
6154
|
+
default: return;
|
|
6155
|
+
}
|
|
6156
|
+
};
|
|
6157
|
+
const applyFirebaseWhere = (initial, where) => {
|
|
6158
|
+
let query = initial;
|
|
6159
|
+
for (const condition of where) {
|
|
6160
|
+
const operator = firestoreOperator(condition.operator);
|
|
6161
|
+
if (condition.connector === "OR" || operator === void 0) throw new FirebaseDatabaseConstraintError("query.unsupported");
|
|
6162
|
+
query = query.where(condition.field, operator, condition.value);
|
|
6163
|
+
}
|
|
6164
|
+
return query;
|
|
6165
|
+
};
|
|
5922
6166
|
const firebaseDatabase = (config) => {
|
|
5923
|
-
const
|
|
5924
|
-
const
|
|
6167
|
+
const implementation = (() => {
|
|
6168
|
+
const app = (0, firebase_admin_app.getApps)().length ? (0, firebase_admin_app.getApp)() : (0, firebase_admin_app.initializeApp)(config);
|
|
6169
|
+
const db = (0, firebase_admin_firestore.getFirestore)(app);
|
|
5925
6170
|
const collections = createFirebaseDatabaseCollections(db);
|
|
5926
6171
|
let migration;
|
|
5927
6172
|
const ensureMigrated = () => {
|
|
5928
|
-
migration ??= migrateFirebaseDatabase(
|
|
6173
|
+
migration ??= migrateFirebaseDatabase(collections).catch((error) => {
|
|
5929
6174
|
migration = void 0;
|
|
5930
6175
|
throw error;
|
|
5931
6176
|
});
|
|
@@ -5948,13 +6193,52 @@ const firebaseDatabase = (config) => {
|
|
|
5948
6193
|
};
|
|
5949
6194
|
const read = async (operation) => {
|
|
5950
6195
|
await ensureMigrated();
|
|
5951
|
-
|
|
6196
|
+
const snapshot = await loadFirebaseDatabaseSnapshot(collections);
|
|
6197
|
+
return operation(createFirebaseDatabaseState(snapshot));
|
|
5952
6198
|
};
|
|
5953
6199
|
return {
|
|
5954
|
-
|
|
6200
|
+
recordInsights: async ({ event }) => {
|
|
6201
|
+
await ensureMigrated();
|
|
6202
|
+
await db.runTransaction(async (transaction) => {
|
|
6203
|
+
const eventReference = collections.bundleEvents.doc(event.id);
|
|
6204
|
+
const installationReference = collections.insightsLatest.doc(firebaseInstallationDocumentId(event.install_id));
|
|
6205
|
+
const [storedEvent, storedInstallation] = await transaction.getAll(eventReference, installationReference);
|
|
6206
|
+
if (storedEvent.exists) return;
|
|
6207
|
+
const current = storedInstallation.exists ? requireFirebaseDocumentKey("insights_latest", storedInstallation.id, parseFirebaseBundleEventRow(storedInstallation.data(), `insights_latest/${storedInstallation.id}`)) : null;
|
|
6208
|
+
transaction.create(eventReference, event);
|
|
6209
|
+
if (current === null || event.received_at_ms > current.received_at_ms || event.received_at_ms === current.received_at_ms && compareInsightsText(event.id, current.id) > 0) transaction.set(installationReference, event);
|
|
6210
|
+
});
|
|
6211
|
+
},
|
|
6212
|
+
findLatestInsightsEvents: async (input) => {
|
|
6213
|
+
await ensureMigrated();
|
|
6214
|
+
if ("installId" in input) {
|
|
6215
|
+
const document = await collections.insightsLatest.doc(firebaseInstallationDocumentId(input.installId)).get();
|
|
6216
|
+
return document.exists ? [requireFirebaseDocumentKey("insights_latest", document.id, parseFirebaseBundleEventRow(document.data(), `insights_latest/${document.id}`))] : [];
|
|
6217
|
+
}
|
|
6218
|
+
return (await applyFirebaseWhere(collections.insightsLatest, latestInsightsWhere(input)).orderBy("install_id", "asc").limit(input.limit).get()).docs.map((document) => requireFirebaseDocumentKey("insights_latest", document.id, parseFirebaseBundleEventRow(document.data(), `insights_latest/${document.id}`)));
|
|
6219
|
+
},
|
|
6220
|
+
countLatestInsightsEvents: async (input) => {
|
|
6221
|
+
await ensureMigrated();
|
|
6222
|
+
const filter = firebase_admin_firestore.Filter.or(...latestInsightsCountGroups(input).map((where) => firebase_admin_firestore.Filter.and(...where.map((condition) => {
|
|
6223
|
+
const operator = firestoreOperator(condition.operator);
|
|
6224
|
+
if (operator === void 0) throw new FirebaseDatabaseConstraintError("query.unsupported");
|
|
6225
|
+
return firebase_admin_firestore.Filter.where(condition.field, operator, condition.value);
|
|
6226
|
+
}))));
|
|
6227
|
+
return (await collections.insightsLatest.where(filter).count().get()).data().count;
|
|
6228
|
+
},
|
|
6229
|
+
create: async (input) => {
|
|
6230
|
+
if (input.model !== "bundle_events") return mutate((database) => database.create(input));
|
|
6231
|
+
await ensureMigrated();
|
|
6232
|
+
await collections.bundleEvents.doc(input.data.id).create(input.data);
|
|
6233
|
+
return input.data;
|
|
6234
|
+
},
|
|
5955
6235
|
update: (input) => mutate((database) => database.update(input)),
|
|
5956
6236
|
delete: (input) => mutate((database) => database.delete(input)),
|
|
5957
|
-
count: (input) =>
|
|
6237
|
+
count: async (input) => {
|
|
6238
|
+
if (input.model !== "bundle_events") return read((database) => database.count(input));
|
|
6239
|
+
await ensureMigrated();
|
|
6240
|
+
return (await applyFirebaseWhere(collections.bundleEvents, input.where ?? []).orderBy("received_at_ms", "desc").orderBy("id", "desc").count().get()).data().count;
|
|
6241
|
+
},
|
|
5958
6242
|
findOne: async (input) => {
|
|
5959
6243
|
const id = exactId(input);
|
|
5960
6244
|
if (id === void 0) return read((database) => database.findOne(input));
|
|
@@ -5976,9 +6260,20 @@ const firebaseDatabase = (config) => {
|
|
|
5976
6260
|
}
|
|
5977
6261
|
},
|
|
5978
6262
|
findMany: async (input) => {
|
|
5979
|
-
if (input.model
|
|
5980
|
-
|
|
5981
|
-
|
|
6263
|
+
if (input.model === "bundle_events") {
|
|
6264
|
+
await ensureMigrated();
|
|
6265
|
+
let query = applyFirebaseWhere(collections.bundleEvents, input.where ?? []);
|
|
6266
|
+
for (const order of input.orderBy ?? []) {
|
|
6267
|
+
if (order.nulls !== void 0) throw new FirebaseDatabaseConstraintError("query.unsupported");
|
|
6268
|
+
query = query.orderBy(order.field, order.direction);
|
|
6269
|
+
}
|
|
6270
|
+
return (await query.offset(input.offset).limit(input.limit).get()).docs.map((document) => requireFirebaseDocumentKey("bundle_events", document.id, parseFirebaseBundleEventRow(document.data(), `bundle_events/${document.id}`)));
|
|
6271
|
+
}
|
|
6272
|
+
if (input.model === "channels") {
|
|
6273
|
+
await ensureMigrated();
|
|
6274
|
+
return queryFirebaseDatabaseRows(await loadFirebaseChannels(collections), input);
|
|
6275
|
+
}
|
|
6276
|
+
return read((database) => database.findMany(input));
|
|
5982
6277
|
},
|
|
5983
6278
|
insertChannel: async (input) => {
|
|
5984
6279
|
await ensureMigrated();
|
|
@@ -6030,7 +6325,8 @@ const firebaseDatabase = (config) => {
|
|
|
6030
6325
|
},
|
|
6031
6326
|
transaction: (callback) => mutate(callback)
|
|
6032
6327
|
};
|
|
6033
|
-
})()
|
|
6328
|
+
})();
|
|
6329
|
+
const adapter = createDatabasePluginAdapter("firebaseDatabase", implementation);
|
|
6034
6330
|
return createDatabasePlugin({
|
|
6035
6331
|
name: "firebaseDatabase",
|
|
6036
6332
|
models: adapter.models,
|
|
@@ -6040,7 +6336,8 @@ const firebaseDatabase = (config) => {
|
|
|
6040
6336
|
//#endregion
|
|
6041
6337
|
//#region src/firebaseStorage.ts
|
|
6042
6338
|
const firebaseStorage = (config) => {
|
|
6043
|
-
const
|
|
6339
|
+
const app = (0, firebase_admin_app.getApps)().length ? (0, firebase_admin_app.getApp)() : (0, firebase_admin_app.initializeApp)(config);
|
|
6340
|
+
const bucket = (0, firebase_admin_storage.getStorage)(app).bucket(config.storageBucket);
|
|
6044
6341
|
const getStorageKey = createStorageKeyBuilder(config.basePath);
|
|
6045
6342
|
const parseAndValidate = (storageUri) => {
|
|
6046
6343
|
const parsed = parseStorageUri(storageUri, "gs");
|
|
@@ -6134,7 +6431,7 @@ const hot = { updater: { v1: (0, firebase_functions_v2_https.onRequest)({ region
|
|
|
6134
6431
|
const request = new Request(fullUrl, {
|
|
6135
6432
|
method: req.method,
|
|
6136
6433
|
headers: req.headers,
|
|
6137
|
-
body: req.method !== "GET" && req.method !== "HEAD" ? req.
|
|
6434
|
+
body: req.method !== "GET" && req.method !== "HEAD" ? new Uint8Array(req.rawBody) : void 0
|
|
6138
6435
|
});
|
|
6139
6436
|
const honoResponse = await app.fetch(request);
|
|
6140
6437
|
res.status(honoResponse.status);
|