@hot-updater/firebase 1.0.0-rc.0 → 1.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/firebase/functions/index.cjs +296 -296
- package/package.json +6 -6
|
@@ -6,7 +6,7 @@ let firebase_admin_firestore = require("firebase-admin/firestore");
|
|
|
6
6
|
let firebase_admin_storage = require("firebase-admin/storage");
|
|
7
7
|
//#endregion
|
|
8
8
|
//#region ../../packages/server/dist/version.mjs
|
|
9
|
-
const HOT_UPDATER_SERVER_VERSION = "1.0.0-rc.
|
|
9
|
+
const HOT_UPDATER_SERVER_VERSION = "1.0.0-rc.1";
|
|
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";
|
|
@@ -947,7 +709,7 @@ const databaseFields = {
|
|
|
947
709
|
};
|
|
948
710
|
//#endregion
|
|
949
711
|
//#region ../plugin-core/dist/databasePluginCrudValidationFields.mjs
|
|
950
|
-
const isRecord$
|
|
712
|
+
const isRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
951
713
|
const isChannelText = (value) => {
|
|
952
714
|
if (typeof value !== "string" || value.length === 0) return false;
|
|
953
715
|
let codePointCount = 0;
|
|
@@ -1233,7 +995,7 @@ const hasValidReleaseInvariants = (data) => {
|
|
|
1233
995
|
};
|
|
1234
996
|
const hasValidBundleEventInvariants = (data) => (data.type === "UPDATE_APPLIED" || data.type === "RECOVERED" || data.type === "RELEASE_ADOPTED") && typeof data.from_bundle_id === "string" && (data.update_strategy === "fingerprint" || data.update_strategy === "appVersion") || data.type === "UNCHANGED" && data.from_bundle_id === null && data.update_strategy === null;
|
|
1235
997
|
const validateCreateData = (model, data) => {
|
|
1236
|
-
if (!isRecord$
|
|
998
|
+
if (!isRecord$2(data)) throw new DatabasePluginInputError("invalid-data");
|
|
1237
999
|
validateFields(model, Object.keys(data));
|
|
1238
1000
|
for (const field of databaseFields[model]) {
|
|
1239
1001
|
const validator = modelValidators[model][field];
|
|
@@ -1250,7 +1012,7 @@ const selectRow = (row, input) => {
|
|
|
1250
1012
|
return Object.fromEntries(select.map((field) => [field, Reflect.get(row, field)]));
|
|
1251
1013
|
};
|
|
1252
1014
|
const validateResult = (model, row, select) => {
|
|
1253
|
-
if (!isRecord$
|
|
1015
|
+
if (!isRecord$2(row)) throw new DatabasePluginInputError("invalid-result");
|
|
1254
1016
|
const fields = select ?? databaseFields[model];
|
|
1255
1017
|
for (const field of fields) {
|
|
1256
1018
|
const validator = modelValidators[model][field];
|
|
@@ -1278,10 +1040,10 @@ const validateMutationWhere = (where) => {
|
|
|
1278
1040
|
const validateUpdateWhere = (model, where) => {
|
|
1279
1041
|
const selector = where[0];
|
|
1280
1042
|
const primaryField = model === "release_catalogs" ? "scope_key" : "id";
|
|
1281
|
-
if (where.length !== 1 || !isRecord$
|
|
1043
|
+
if (where.length !== 1 || !isRecord$2(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
1044
|
};
|
|
1283
1045
|
const validateBundleUpdateData = (update) => {
|
|
1284
|
-
if (!isRecord$
|
|
1046
|
+
if (!isRecord$2(update)) throw new DatabasePluginInputError("invalid-data");
|
|
1285
1047
|
for (const [field, value] of Object.entries(update)) {
|
|
1286
1048
|
if (field === "id") throw new DatabasePluginInputError("invalid-data");
|
|
1287
1049
|
validateField("bundles", field);
|
|
@@ -1290,7 +1052,7 @@ const validateBundleUpdateData = (update) => {
|
|
|
1290
1052
|
}
|
|
1291
1053
|
};
|
|
1292
1054
|
const validateApiKeyUpdateData = (update) => {
|
|
1293
|
-
if (!isRecord$
|
|
1055
|
+
if (!isRecord$2(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
1056
|
};
|
|
1295
1057
|
const RELEASE_MUTABLE_FIELDS = new Set([
|
|
1296
1058
|
"revision",
|
|
@@ -1305,7 +1067,7 @@ const RELEASE_MUTABLE_FIELDS = new Set([
|
|
|
1305
1067
|
"updated_at_ms"
|
|
1306
1068
|
]);
|
|
1307
1069
|
const validateReleaseUpdateData = (update) => {
|
|
1308
|
-
if (!isRecord$
|
|
1070
|
+
if (!isRecord$2(update) || Reflect.ownKeys(update).length === 0) throw new DatabasePluginInputError("invalid-data");
|
|
1309
1071
|
for (const [field, value] of Object.entries(update)) {
|
|
1310
1072
|
if (!RELEASE_MUTABLE_FIELDS.has(field)) throw new DatabasePluginInputError("invalid-data");
|
|
1311
1073
|
const validator = modelValidators.releases[field];
|
|
@@ -1313,7 +1075,7 @@ const validateReleaseUpdateData = (update) => {
|
|
|
1313
1075
|
}
|
|
1314
1076
|
};
|
|
1315
1077
|
const validateReleaseCatalogUpdateData = (update) => {
|
|
1316
|
-
if (!isRecord$
|
|
1078
|
+
if (!isRecord$2(update)) throw new DatabasePluginInputError("invalid-data");
|
|
1317
1079
|
const expectedFields = Object.keys(modelValidators.release_catalogs).filter((field) => field !== "scope_key");
|
|
1318
1080
|
if (Reflect.ownKeys(update).length !== expectedFields.length || expectedFields.some((field) => !Object.hasOwn(update, field))) throw new DatabasePluginInputError("invalid-data");
|
|
1319
1081
|
for (const [field, value] of Object.entries(update)) {
|
|
@@ -1393,7 +1155,7 @@ const validateWhere$1 = (model, where) => {
|
|
|
1393
1155
|
if (where === void 0) return;
|
|
1394
1156
|
if (!Array.isArray(where)) throw new DatabasePluginInputError("invalid-query");
|
|
1395
1157
|
for (const item of where) {
|
|
1396
|
-
if (!isRecord$
|
|
1158
|
+
if (!isRecord$2(item)) throw new DatabasePluginInputError("invalid-query");
|
|
1397
1159
|
if (item.connector !== void 0 && item.connector !== "AND" && item.connector !== "OR") throw new DatabasePluginInputError("invalid-query");
|
|
1398
1160
|
validateWhereValue(model, item);
|
|
1399
1161
|
}
|
|
@@ -1410,7 +1172,7 @@ const validateOrderBy = (model, orderBy) => {
|
|
|
1410
1172
|
if (!Array.isArray(orderBy) || orderBy.length === 0) throw new DatabasePluginInputError("invalid-query");
|
|
1411
1173
|
const fields = /* @__PURE__ */ new Set();
|
|
1412
1174
|
return orderBy.map((clause) => {
|
|
1413
|
-
if (!isRecord$
|
|
1175
|
+
if (!isRecord$2(clause) || typeof clause.field !== "string") throw new DatabasePluginInputError("invalid-query");
|
|
1414
1176
|
validateField(model, clause.field);
|
|
1415
1177
|
if (!sortableFields[model].has(clause.field)) throw new DatabasePluginInputError("invalid-query");
|
|
1416
1178
|
if (clause.direction !== "asc" && clause.direction !== "desc") throw new DatabasePluginInputError("invalid-query");
|
|
@@ -1422,7 +1184,7 @@ const validateOrderBy = (model, orderBy) => {
|
|
|
1422
1184
|
};
|
|
1423
1185
|
const validateDistinctOn = (model, distinctOn, orderBy) => {
|
|
1424
1186
|
if (distinctOn === void 0) return;
|
|
1425
|
-
if (!isRecord$
|
|
1187
|
+
if (!isRecord$2(distinctOn)) throw new DatabasePluginInputError("invalid-distinct");
|
|
1426
1188
|
const fields = validateDistinctFields(model, distinctOn.fields);
|
|
1427
1189
|
if (fields === void 0 || orderBy === void 0) throw new DatabasePluginInputError("invalid-distinct");
|
|
1428
1190
|
for (const [index, field] of fields.entries()) if (orderBy[index]?.field !== field) throw new DatabasePluginInputError("invalid-distinct");
|
|
@@ -1436,7 +1198,7 @@ const validateBundlePagination = (options) => {
|
|
|
1436
1198
|
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
1199
|
const cursor = options.cursor;
|
|
1438
1200
|
if (cursor === void 0) return;
|
|
1439
|
-
if (!isRecord$
|
|
1201
|
+
if (!isRecord$2(cursor)) throw new DatabasePluginInputError("invalid-pagination");
|
|
1440
1202
|
const hasAfter = Object.hasOwn(cursor, "after");
|
|
1441
1203
|
if (hasAfter === Object.hasOwn(cursor, "before")) throw new DatabasePluginInputError("invalid-pagination");
|
|
1442
1204
|
const value = hasAfter ? cursor.after : cursor.before;
|
|
@@ -1779,7 +1541,7 @@ const applyChange = async (database, change, changeIndex) => {
|
|
|
1779
1541
|
}
|
|
1780
1542
|
return;
|
|
1781
1543
|
}
|
|
1782
|
-
case "
|
|
1544
|
+
case "insights":
|
|
1783
1545
|
await database.create({
|
|
1784
1546
|
model: "bundle_events",
|
|
1785
1547
|
data: change.row
|
|
@@ -1856,10 +1618,10 @@ const hasOnlyKeys = (value, keys) => {
|
|
|
1856
1618
|
return Reflect.ownKeys(value).length === keys.length && keys.every((key) => Object.hasOwn(value, key));
|
|
1857
1619
|
};
|
|
1858
1620
|
const validateWhere = (where, field, validateValue = (value) => typeof value === "string") => {
|
|
1859
|
-
if (!isRecord$
|
|
1621
|
+
if (!isRecord$2(where) || !hasOnlyKeys(where, [field]) || !validateValue(Reflect.get(where, field))) throw new DatabasePluginInputError("invalid-data");
|
|
1860
1622
|
};
|
|
1861
1623
|
const validateDatabaseChange = (change) => {
|
|
1862
|
-
if (!isRecord$
|
|
1624
|
+
if (!isRecord$2(change)) throw new DatabasePluginInputError("invalid-data");
|
|
1863
1625
|
switch (change.model) {
|
|
1864
1626
|
case "bundles": switch (change.operation) {
|
|
1865
1627
|
case "insert":
|
|
@@ -1966,7 +1728,7 @@ const validateDatabaseChange = (change) => {
|
|
|
1966
1728
|
return;
|
|
1967
1729
|
default: throw new DatabasePluginInputError("invalid-operation");
|
|
1968
1730
|
}
|
|
1969
|
-
case "
|
|
1731
|
+
case "insights":
|
|
1970
1732
|
if (change.operation !== "insert" || !hasOnlyKeys(change, [
|
|
1971
1733
|
"model",
|
|
1972
1734
|
"operation",
|
|
@@ -1992,7 +1754,7 @@ const validateDatabaseChange = (change) => {
|
|
|
1992
1754
|
"update"
|
|
1993
1755
|
])) throw new DatabasePluginInputError("invalid-data");
|
|
1994
1756
|
validateWhere(change.where, "id");
|
|
1995
|
-
if (!isRecord$
|
|
1757
|
+
if (!isRecord$2(change.update)) throw new DatabasePluginInputError("invalid-data");
|
|
1996
1758
|
if (!hasOnlyKeys(change.update, ["revokedAtMs"])) throw new DatabasePluginInputError("invalid-data");
|
|
1997
1759
|
validateApiKeyUpdateData({ revoked_at_ms: change.update.revokedAtMs });
|
|
1998
1760
|
return;
|
|
@@ -2002,7 +1764,7 @@ const validateDatabaseChange = (change) => {
|
|
|
2002
1764
|
}
|
|
2003
1765
|
};
|
|
2004
1766
|
const validateDatabaseCommitExpectation = (expectation) => {
|
|
2005
|
-
if (!isRecord$
|
|
1767
|
+
if (!isRecord$2(expectation)) throw new DatabasePluginInputError("invalid-data");
|
|
2006
1768
|
if (expectation.model === "releases") {
|
|
2007
1769
|
if (!hasOnlyKeys(expectation, [
|
|
2008
1770
|
"model",
|
|
@@ -2022,7 +1784,7 @@ const validateDatabaseCommitExpectation = (expectation) => {
|
|
|
2022
1784
|
throw new DatabasePluginInputError("invalid-model");
|
|
2023
1785
|
};
|
|
2024
1786
|
function validateDatabaseCommit(input) {
|
|
2025
|
-
if (!isRecord$
|
|
1787
|
+
if (!isRecord$2(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
1788
|
input.changes.forEach(validateDatabaseChange);
|
|
2027
1789
|
if (Array.isArray(input.expectations)) input.expectations.forEach(validateDatabaseCommitExpectation);
|
|
2028
1790
|
}
|
|
@@ -2241,7 +2003,7 @@ const createDatabasePluginAdapter = (name, implementation) => {
|
|
|
2241
2003
|
return result;
|
|
2242
2004
|
}
|
|
2243
2005
|
},
|
|
2244
|
-
|
|
2006
|
+
insights: {
|
|
2245
2007
|
async append(row) {
|
|
2246
2008
|
await crud.create({
|
|
2247
2009
|
model: "bundle_events",
|
|
@@ -3848,7 +3610,7 @@ const createReleaseCatalogRouteHandlers = (clientAccessHeaderName = "x-api-key")
|
|
|
3848
3610
|
//#endregion
|
|
3849
3611
|
//#region ../../packages/server/dist/handlerReleaseManagementRoutes.mjs
|
|
3850
3612
|
const unavailable = () => Response.json({ error: "Not found" }, { status: 404 });
|
|
3851
|
-
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3613
|
+
const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3852
3614
|
const parseRevision = (value) => {
|
|
3853
3615
|
if (value === void 0 || value === null || value === "") return void 0;
|
|
3854
3616
|
const revision = typeof value === "number" ? value : Number(value);
|
|
@@ -3857,7 +3619,7 @@ const parseRevision = (value) => {
|
|
|
3857
3619
|
};
|
|
3858
3620
|
const parsePolicyInput = async (request) => {
|
|
3859
3621
|
const body = await request.json();
|
|
3860
|
-
if (!isRecord(body) || !isRecord(body.patch)) throw new HandlerBadRequestError("Invalid Release policy mutation");
|
|
3622
|
+
if (!isRecord$1(body) || !isRecord$1(body.patch)) throw new HandlerBadRequestError("Invalid Release policy mutation");
|
|
3861
3623
|
return {
|
|
3862
3624
|
expectedRevision: parseRevision(body.expectedRevision),
|
|
3863
3625
|
patch: body.patch
|
|
@@ -3966,6 +3728,244 @@ const createReleaseManagementRouteHandlers = () => ({
|
|
|
3966
3728
|
}
|
|
3967
3729
|
});
|
|
3968
3730
|
//#endregion
|
|
3731
|
+
//#region ../../packages/server/dist/insights/errors.mjs
|
|
3732
|
+
var InsightsScanLimitExceededError = class extends Error {
|
|
3733
|
+
constructor(limit) {
|
|
3734
|
+
super(`Insights event scan exceeded ${limit} rows.`);
|
|
3735
|
+
this.limit = limit;
|
|
3736
|
+
this.name = "InsightsScanLimitExceededError";
|
|
3737
|
+
}
|
|
3738
|
+
};
|
|
3739
|
+
var InsightsBadRequestError = class extends Error {
|
|
3740
|
+
name = "InsightsBadRequestError";
|
|
3741
|
+
};
|
|
3742
|
+
var InsightsPayloadTooLargeError = class extends Error {
|
|
3743
|
+
name = "InsightsPayloadTooLargeError";
|
|
3744
|
+
constructor(maximumBytes) {
|
|
3745
|
+
super(`Event payload exceeds ${maximumBytes} bytes`);
|
|
3746
|
+
this.maximumBytes = maximumBytes;
|
|
3747
|
+
}
|
|
3748
|
+
};
|
|
3749
|
+
//#endregion
|
|
3750
|
+
//#region ../../packages/server/dist/insights/eventInput.mjs
|
|
3751
|
+
const MAX_EVENT_STRING_LENGTH = 1024;
|
|
3752
|
+
const EVENT_BODY_MAX_BYTES = 16 * 1024;
|
|
3753
|
+
const eventKeys = new Set([
|
|
3754
|
+
"type",
|
|
3755
|
+
"installId",
|
|
3756
|
+
"toBundleId",
|
|
3757
|
+
"userId",
|
|
3758
|
+
"username",
|
|
3759
|
+
"platform",
|
|
3760
|
+
"appVersion",
|
|
3761
|
+
"channel",
|
|
3762
|
+
"cohort",
|
|
3763
|
+
"fingerprintHash",
|
|
3764
|
+
"fromBundleId",
|
|
3765
|
+
"fromReleaseId",
|
|
3766
|
+
"toReleaseId",
|
|
3767
|
+
"updateStrategy",
|
|
3768
|
+
"sdkVersion"
|
|
3769
|
+
]);
|
|
3770
|
+
function isRecord(value) {
|
|
3771
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3772
|
+
}
|
|
3773
|
+
function requireStringField(payload, key) {
|
|
3774
|
+
const value = payload[key];
|
|
3775
|
+
if (typeof value !== "string" || value.length === 0 || value.length > MAX_EVENT_STRING_LENGTH) throw new InsightsBadRequestError(`Invalid event field: ${key}`);
|
|
3776
|
+
return value;
|
|
3777
|
+
}
|
|
3778
|
+
function requireNullableStringField(payload, key) {
|
|
3779
|
+
if (payload[key] === null) return null;
|
|
3780
|
+
return requireStringField(payload, key);
|
|
3781
|
+
}
|
|
3782
|
+
async function readBoundedText(request) {
|
|
3783
|
+
const contentLength = request.headers.get("content-length");
|
|
3784
|
+
const declaredByteLength = Number(contentLength);
|
|
3785
|
+
if (contentLength !== null && Number.isSafeInteger(declaredByteLength) && declaredByteLength > 16384) throw new InsightsPayloadTooLargeError(EVENT_BODY_MAX_BYTES);
|
|
3786
|
+
if (request.body === null) return "";
|
|
3787
|
+
const reader = request.body.getReader();
|
|
3788
|
+
const decoder = new TextDecoder();
|
|
3789
|
+
let byteLength = 0;
|
|
3790
|
+
let text = "";
|
|
3791
|
+
while (true) {
|
|
3792
|
+
const result = await reader.read();
|
|
3793
|
+
if (result.done) break;
|
|
3794
|
+
byteLength += result.value.byteLength;
|
|
3795
|
+
if (byteLength > 16384) {
|
|
3796
|
+
await reader.cancel();
|
|
3797
|
+
throw new InsightsPayloadTooLargeError(EVENT_BODY_MAX_BYTES);
|
|
3798
|
+
}
|
|
3799
|
+
text += decoder.decode(result.value, { stream: true });
|
|
3800
|
+
}
|
|
3801
|
+
return text + decoder.decode();
|
|
3802
|
+
}
|
|
3803
|
+
async function parseJson(request) {
|
|
3804
|
+
const text = await readBoundedText(request);
|
|
3805
|
+
try {
|
|
3806
|
+
return JSON.parse(text);
|
|
3807
|
+
} catch (error) {
|
|
3808
|
+
if (error instanceof SyntaxError) throw new InsightsBadRequestError("Invalid event payload");
|
|
3809
|
+
throw error;
|
|
3810
|
+
}
|
|
3811
|
+
}
|
|
3812
|
+
function requireEvent(payload) {
|
|
3813
|
+
if (!isRecord(payload) || Object.keys(payload).some((key) => !eventKeys.has(key))) throw new InsightsBadRequestError("Invalid event payload");
|
|
3814
|
+
const platform = requireStringField(payload, "platform");
|
|
3815
|
+
if (platform !== "ios" && platform !== "android") throw new InsightsBadRequestError("Invalid event field: platform");
|
|
3816
|
+
const base = {
|
|
3817
|
+
installId: requireStringField(payload, "installId"),
|
|
3818
|
+
toBundleId: requireStringField(payload, "toBundleId"),
|
|
3819
|
+
...payload.userId === void 0 ? {} : { userId: requireStringField(payload, "userId") },
|
|
3820
|
+
...payload.username === void 0 ? {} : { username: requireStringField(payload, "username") },
|
|
3821
|
+
platform,
|
|
3822
|
+
appVersion: requireStringField(payload, "appVersion"),
|
|
3823
|
+
channel: requireStringField(payload, "channel"),
|
|
3824
|
+
cohort: requireStringField(payload, "cohort"),
|
|
3825
|
+
fingerprintHash: requireNullableStringField(payload, "fingerprintHash"),
|
|
3826
|
+
sdkVersion: payload.sdkVersion === void 0 ? null : requireNullableStringField(payload, "sdkVersion"),
|
|
3827
|
+
fromReleaseId: requireNullableStringField(payload, "fromReleaseId"),
|
|
3828
|
+
toReleaseId: requireNullableStringField(payload, "toReleaseId")
|
|
3829
|
+
};
|
|
3830
|
+
const type = requireStringField(payload, "type");
|
|
3831
|
+
switch (type) {
|
|
3832
|
+
case "UPDATE_APPLIED":
|
|
3833
|
+
case "RECOVERED":
|
|
3834
|
+
case "RELEASE_ADOPTED": {
|
|
3835
|
+
const updateStrategy = requireStringField(payload, "updateStrategy");
|
|
3836
|
+
if (updateStrategy !== "fingerprint" && updateStrategy !== "appVersion") throw new InsightsBadRequestError("Invalid event field: updateStrategy");
|
|
3837
|
+
return {
|
|
3838
|
+
...base,
|
|
3839
|
+
type,
|
|
3840
|
+
fromBundleId: requireStringField(payload, "fromBundleId"),
|
|
3841
|
+
updateStrategy
|
|
3842
|
+
};
|
|
3843
|
+
}
|
|
3844
|
+
case "UNCHANGED":
|
|
3845
|
+
if (payload.fromBundleId !== null || payload.updateStrategy !== null) throw new InsightsBadRequestError("Invalid unchanged event shape");
|
|
3846
|
+
return {
|
|
3847
|
+
...base,
|
|
3848
|
+
type,
|
|
3849
|
+
fromBundleId: null,
|
|
3850
|
+
updateStrategy: null
|
|
3851
|
+
};
|
|
3852
|
+
default: throw new InsightsBadRequestError("Invalid event field: type");
|
|
3853
|
+
}
|
|
3854
|
+
}
|
|
3855
|
+
async function parseBundleEventRequest(request) {
|
|
3856
|
+
return requireEvent(await parseJson(request));
|
|
3857
|
+
}
|
|
3858
|
+
//#endregion
|
|
3859
|
+
//#region ../../packages/server/dist/insights/queryInput.mjs
|
|
3860
|
+
const EVENT_LIST_BOUNDS = {
|
|
3861
|
+
defaultValue: 50,
|
|
3862
|
+
maximum: 100,
|
|
3863
|
+
minimum: 1
|
|
3864
|
+
};
|
|
3865
|
+
const EVENT_LIST_OFFSET_BOUNDS = {
|
|
3866
|
+
defaultValue: 0,
|
|
3867
|
+
minimum: 0
|
|
3868
|
+
};
|
|
3869
|
+
const MAX_USER_ID_LENGTH = 1024;
|
|
3870
|
+
function parseInteger(url, key, bounds) {
|
|
3871
|
+
const value = url.searchParams.get(key);
|
|
3872
|
+
if (value === null) return bounds.defaultValue;
|
|
3873
|
+
const parsed = Number(value);
|
|
3874
|
+
if (!Number.isSafeInteger(parsed) || parsed < bounds.minimum || bounds.maximum !== void 0 && parsed > bounds.maximum) throw new InsightsBadRequestError(`Invalid '${key}' query parameter.`);
|
|
3875
|
+
return parsed;
|
|
3876
|
+
}
|
|
3877
|
+
const parsePagination = (request) => {
|
|
3878
|
+
const url = new URL(request.url);
|
|
3879
|
+
return {
|
|
3880
|
+
limit: parseInteger(url, "limit", EVENT_LIST_BOUNDS),
|
|
3881
|
+
offset: parseInteger(url, "offset", EVENT_LIST_OFFSET_BOUNDS)
|
|
3882
|
+
};
|
|
3883
|
+
};
|
|
3884
|
+
const parseInsightsQuery = (request) => {
|
|
3885
|
+
const window = new URL(request.url).searchParams.get("window") ?? "24h";
|
|
3886
|
+
if (window !== "24h" && window !== "7d" && window !== "30d" && window !== "all") throw new InsightsBadRequestError("Invalid 'window' query parameter.");
|
|
3887
|
+
return {
|
|
3888
|
+
...parsePagination(request),
|
|
3889
|
+
window
|
|
3890
|
+
};
|
|
3891
|
+
};
|
|
3892
|
+
const parseActiveInstallationInput = (request) => {
|
|
3893
|
+
const url = new URL(request.url);
|
|
3894
|
+
const windows = url.searchParams.getAll("window");
|
|
3895
|
+
if (windows.length > 1) throw new InsightsBadRequestError("Duplicate 'window' query parameter.");
|
|
3896
|
+
const window = windows[0] ?? "30d";
|
|
3897
|
+
if (window !== "24h" && window !== "7d" && window !== "30d") throw new InsightsBadRequestError("Invalid 'window' query parameter.");
|
|
3898
|
+
const userIds = url.searchParams.getAll("userId");
|
|
3899
|
+
if (userIds.length > 1) throw new InsightsBadRequestError("Duplicate 'userId' query parameter.");
|
|
3900
|
+
const userId = userIds[0];
|
|
3901
|
+
if (userId !== void 0 && (userId.length === 0 || userId.length > MAX_USER_ID_LENGTH)) throw new InsightsBadRequestError("Invalid 'userId' query parameter.");
|
|
3902
|
+
return userId === void 0 ? { window } : {
|
|
3903
|
+
window,
|
|
3904
|
+
userId
|
|
3905
|
+
};
|
|
3906
|
+
};
|
|
3907
|
+
const parseSearchInput = (request) => ({
|
|
3908
|
+
...parsePagination(request),
|
|
3909
|
+
query: new URL(request.url).searchParams.get("query")?.trim() ?? ""
|
|
3910
|
+
});
|
|
3911
|
+
//#endregion
|
|
3912
|
+
//#region ../../packages/server/dist/insights/routes.mjs
|
|
3913
|
+
const json = (body, status) => Response.json(body, {
|
|
3914
|
+
headers: { "cache-control": "private, no-store" },
|
|
3915
|
+
status
|
|
3916
|
+
});
|
|
3917
|
+
const requireParam = (params, key) => {
|
|
3918
|
+
const value = params[key];
|
|
3919
|
+
if (value === void 0 || value.length === 0) throw new InsightsBadRequestError(`Missing route parameter: ${key}`);
|
|
3920
|
+
return value;
|
|
3921
|
+
};
|
|
3922
|
+
const run = async (operation) => {
|
|
3923
|
+
try {
|
|
3924
|
+
return await operation();
|
|
3925
|
+
} catch (error) {
|
|
3926
|
+
if (error instanceof InsightsBadRequestError) return json({ error: error.message }, 400);
|
|
3927
|
+
if (error instanceof InsightsPayloadTooLargeError) return json({ error: error.message }, 413);
|
|
3928
|
+
if (error instanceof InsightsScanLimitExceededError) return json({ error: {
|
|
3929
|
+
code: "INSIGHTS_SCAN_LIMIT_EXCEEDED",
|
|
3930
|
+
limit: error.limit
|
|
3931
|
+
} }, 503);
|
|
3932
|
+
throw error;
|
|
3933
|
+
}
|
|
3934
|
+
};
|
|
3935
|
+
const query = (operation) => run(async () => json(await operation(), 200));
|
|
3936
|
+
const createInsightsRouteHandlers = (provider) => ({
|
|
3937
|
+
appendBundleEvent: async (_params, request) => run(async () => {
|
|
3938
|
+
await provider.appendBundleEvent(await parseBundleEventRequest(request));
|
|
3939
|
+
return new Response(null, { status: 204 });
|
|
3940
|
+
}),
|
|
3941
|
+
getBundleEventSummary: (params) => query(() => provider.getBundleEventSummary(requireParam(params, "id"))),
|
|
3942
|
+
getBundleEventInsights: (params, request) => query(() => {
|
|
3943
|
+
const input = parseInsightsQuery(request);
|
|
3944
|
+
return provider.getBundleEventInsights(requireParam(params, "id"), input.window, input.limit, input.offset);
|
|
3945
|
+
}),
|
|
3946
|
+
getBundleEventOverview: () => query(() => provider.getBundleEventOverview()),
|
|
3947
|
+
getActiveInstallationOverview: (_params, request) => query(() => provider.getActiveInstallationOverview(parseActiveInstallationInput(request))),
|
|
3948
|
+
searchInstallations: (_params, request) => query(() => {
|
|
3949
|
+
const input = parseSearchInput(request);
|
|
3950
|
+
return provider.searchInstallations(input.query, input.limit, input.offset);
|
|
3951
|
+
}),
|
|
3952
|
+
getInstallationHistory: (params, request) => query(() => {
|
|
3953
|
+
const input = parsePagination(request);
|
|
3954
|
+
return provider.getInstallationHistory(requireParam(params, "installId"), input.limit, input.offset);
|
|
3955
|
+
})
|
|
3956
|
+
});
|
|
3957
|
+
const registerInsightsClientRoutes = (add) => {
|
|
3958
|
+
add("POST", "/events", "appendBundleEvent");
|
|
3959
|
+
};
|
|
3960
|
+
const registerInsightsAdminRoutes = (add) => {
|
|
3961
|
+
add("GET", "/bundles/:id/events/summary", "getBundleEventSummary");
|
|
3962
|
+
add("GET", "/bundles/:id/events/insights", "getBundleEventInsights");
|
|
3963
|
+
add("GET", "/installations/overview", "getBundleEventOverview");
|
|
3964
|
+
add("GET", "/installations/active", "getActiveInstallationOverview");
|
|
3965
|
+
add("GET", "/installations", "searchInstallations");
|
|
3966
|
+
add("GET", "/installations/:installId/events", "getInstallationHistory");
|
|
3967
|
+
};
|
|
3968
|
+
//#endregion
|
|
3969
3969
|
//#region ../../packages/server/dist/internalRouter.mjs
|
|
3970
3970
|
const normalizePath = (path) => {
|
|
3971
3971
|
if (!path) return "/";
|
|
@@ -4072,13 +4072,13 @@ const createDownloadStorageRouteHandler = (downloadStorageObject) => async (para
|
|
|
4072
4072
|
statusText: response.statusText
|
|
4073
4073
|
});
|
|
4074
4074
|
};
|
|
4075
|
-
function createHotUpdaterHandlers(api,
|
|
4075
|
+
function createHotUpdaterHandlers(api, insights, apiKeyAuth, downloadStorageObject) {
|
|
4076
4076
|
const routeHandlers = {
|
|
4077
4077
|
...createVersionRouteHandlers(),
|
|
4078
4078
|
...createReleaseCatalogRouteHandlers(apiKeyAuth?.headerName),
|
|
4079
4079
|
...createReleaseManagementRouteHandlers(),
|
|
4080
4080
|
...createBundleRouteHandlers(),
|
|
4081
|
-
...
|
|
4081
|
+
...insights === void 0 ? {} : createInsightsRouteHandlers(insights),
|
|
4082
4082
|
...downloadStorageObject === void 0 ? {} : { downloadStorageObject: createDownloadStorageRouteHandler(downloadStorageObject) }
|
|
4083
4083
|
};
|
|
4084
4084
|
const clientRouter = createRouter();
|
|
@@ -4088,7 +4088,7 @@ function createHotUpdaterHandlers(api, analytics, apiKeyAuth, downloadStorageObj
|
|
|
4088
4088
|
addClientRoute("GET", "/release-catalogs/app-version/:platform/:channelKey/:appVersion", "appVersionReleaseCatalog");
|
|
4089
4089
|
addClientRoute("GET", "/release-catalogs/fingerprint/:platform/:channelKey/:fingerprintHash", "fingerprintReleaseCatalog");
|
|
4090
4090
|
addClientRoute("GET", "/artifacts/:targetBundleId/from/:currentBundleId", "artifact");
|
|
4091
|
-
if (
|
|
4091
|
+
if (insights !== void 0) registerInsightsClientRoutes(addClientRoute);
|
|
4092
4092
|
const adminRouter = createRouter();
|
|
4093
4093
|
const addAdminRoute = (method, path, handler) => addRoute(adminRouter, method, path, handler);
|
|
4094
4094
|
addAdminRoute("GET", "/releases/:id", "getRelease");
|
|
@@ -4108,7 +4108,7 @@ function createHotUpdaterHandlers(api, analytics, apiKeyAuth, downloadStorageObj
|
|
|
4108
4108
|
addAdminRoute("POST", "/bundles", "createBundles");
|
|
4109
4109
|
addAdminRoute("PATCH", "/bundles/:id", "updateBundle");
|
|
4110
4110
|
addAdminRoute("DELETE", "/bundles/:id", "deleteBundle");
|
|
4111
|
-
if (
|
|
4111
|
+
if (insights !== void 0) registerInsightsAdminRoutes(addAdminRoute);
|
|
4112
4112
|
return Object.freeze({
|
|
4113
4113
|
client: createRequestHandler({
|
|
4114
4114
|
api,
|
|
@@ -4125,7 +4125,7 @@ function createHotUpdaterHandlers(api, analytics, apiKeyAuth, downloadStorageObj
|
|
|
4125
4125
|
});
|
|
4126
4126
|
}
|
|
4127
4127
|
//#endregion
|
|
4128
|
-
//#region ../../packages/server/dist/
|
|
4128
|
+
//#region ../../packages/server/dist/insights/bounded/activeOverview.mjs
|
|
4129
4129
|
const HOUR_MS = 3600 * 1e3;
|
|
4130
4130
|
const DAY_MS = 24 * HOUR_MS;
|
|
4131
4131
|
const ACTIVE_BUNDLE_EVENT_TYPES = [
|
|
@@ -4209,11 +4209,11 @@ function collectActiveInstallationOverview(request) {
|
|
|
4209
4209
|
};
|
|
4210
4210
|
}
|
|
4211
4211
|
//#endregion
|
|
4212
|
-
//#region ../../packages/server/dist/
|
|
4213
|
-
const
|
|
4214
|
-
const
|
|
4215
|
-
const
|
|
4216
|
-
const
|
|
4212
|
+
//#region ../../packages/server/dist/insights/bounded/scan.mjs
|
|
4213
|
+
const INSIGHTS_SCAN_MAX_ROWS = 5e4;
|
|
4214
|
+
const INSIGHTS_MATERIALIZATION_LIMIT = INSIGHTS_SCAN_MAX_ROWS + 1;
|
|
4215
|
+
const INSIGHTS_SCAN_PAGE_SIZE = 1e3;
|
|
4216
|
+
const INSIGHTS_LOWER_BOUND_ID = "00000000-0000-0000-0000-000000000000";
|
|
4217
4217
|
const compareCodePoints = (left, right) => {
|
|
4218
4218
|
if (left < right) return -1;
|
|
4219
4219
|
if (left > right) return 1;
|
|
@@ -4226,23 +4226,23 @@ const materializeEventRows = async (scope) => {
|
|
|
4226
4226
|
const seenIds = /* @__PURE__ */ new Set();
|
|
4227
4227
|
let after = scope.lowerBoundMs === void 0 ? void 0 : {
|
|
4228
4228
|
receivedAtMs: scope.lowerBoundMs,
|
|
4229
|
-
id:
|
|
4229
|
+
id: INSIGHTS_LOWER_BOUND_ID
|
|
4230
4230
|
};
|
|
4231
|
-
while (rows.length <
|
|
4232
|
-
const limit = Math.min(
|
|
4231
|
+
while (rows.length < INSIGHTS_MATERIALIZATION_LIMIT) {
|
|
4232
|
+
const limit = Math.min(INSIGHTS_SCAN_PAGE_SIZE, INSIGHTS_MATERIALIZATION_LIMIT - rows.length);
|
|
4233
4233
|
const page = await scope.persistence.scan({
|
|
4234
4234
|
beforeReceivedAtMs: scope.cutoffMs,
|
|
4235
4235
|
...after === void 0 ? {} : { after },
|
|
4236
4236
|
limit
|
|
4237
4237
|
});
|
|
4238
4238
|
if (page.length === 0) break;
|
|
4239
|
-
if (page.length > limit) throw new
|
|
4239
|
+
if (page.length > limit) throw new InsightsPersistenceOrderError();
|
|
4240
4240
|
let previous = after === void 0 ? void 0 : {
|
|
4241
4241
|
received_at_ms: after.receivedAtMs,
|
|
4242
4242
|
id: after.id
|
|
4243
4243
|
};
|
|
4244
4244
|
for (const row of page) {
|
|
4245
|
-
if (row.received_at_ms >= scope.cutoffMs || previous !== void 0 && compareEventOldest(previous, row) >= 0 || seenIds.has(row.id)) throw new
|
|
4245
|
+
if (row.received_at_ms >= scope.cutoffMs || previous !== void 0 && compareEventOldest(previous, row) >= 0 || seenIds.has(row.id)) throw new InsightsPersistenceOrderError();
|
|
4246
4246
|
seenIds.add(row.id);
|
|
4247
4247
|
rows.push(row);
|
|
4248
4248
|
previous = row;
|
|
@@ -4252,14 +4252,14 @@ const materializeEventRows = async (scope) => {
|
|
|
4252
4252
|
receivedAtMs: last.received_at_ms,
|
|
4253
4253
|
id: last.id
|
|
4254
4254
|
};
|
|
4255
|
-
if (rows.length > 5e4) throw new
|
|
4255
|
+
if (rows.length > 5e4) throw new InsightsScanLimitExceededError(INSIGHTS_SCAN_MAX_ROWS);
|
|
4256
4256
|
}
|
|
4257
4257
|
return rows;
|
|
4258
4258
|
};
|
|
4259
|
-
var
|
|
4260
|
-
name = "
|
|
4259
|
+
var InsightsPersistenceOrderError = class extends Error {
|
|
4260
|
+
name = "InsightsPersistenceOrderError";
|
|
4261
4261
|
constructor() {
|
|
4262
|
-
super("
|
|
4262
|
+
super("Insights persistence did not advance its scan cursor.");
|
|
4263
4263
|
}
|
|
4264
4264
|
};
|
|
4265
4265
|
const materializeActiveRows = async (scope, window) => {
|
|
@@ -4344,7 +4344,7 @@ const collectEventActivity = (request) => {
|
|
|
4344
4344
|
};
|
|
4345
4345
|
};
|
|
4346
4346
|
//#endregion
|
|
4347
|
-
//#region ../../packages/server/dist/
|
|
4347
|
+
//#region ../../packages/server/dist/insights/bounded/installationSearch.mjs
|
|
4348
4348
|
function toSearchRow(row) {
|
|
4349
4349
|
return {
|
|
4350
4350
|
installId: row.install_id,
|
|
@@ -4384,7 +4384,7 @@ function searchEventInstallations(request) {
|
|
|
4384
4384
|
};
|
|
4385
4385
|
}
|
|
4386
4386
|
//#endregion
|
|
4387
|
-
//#region ../../packages/server/dist/
|
|
4387
|
+
//#region ../../packages/server/dist/insights/bounded/persistence.mjs
|
|
4388
4388
|
function createBundleEventRow(input) {
|
|
4389
4389
|
const base = {
|
|
4390
4390
|
id: createUUIDv7(),
|
|
@@ -4420,7 +4420,7 @@ function createBundleEventRow(input) {
|
|
|
4420
4420
|
}
|
|
4421
4421
|
}
|
|
4422
4422
|
//#endregion
|
|
4423
|
-
//#region ../../packages/server/dist/
|
|
4423
|
+
//#region ../../packages/server/dist/insights/bounded/provider.mjs
|
|
4424
4424
|
const isTransitionEventRow = (row) => row.type === "UPDATE_APPLIED" || row.type === "RECOVERED";
|
|
4425
4425
|
const toHistoryRow = (row) => ({
|
|
4426
4426
|
id: row.id,
|
|
@@ -4438,7 +4438,7 @@ const toHistoryRow = (row) => ({
|
|
|
4438
4438
|
const isInstalledForBundle = (row, bundleId) => row.type === "UPDATE_APPLIED" && row.to_bundle_id === bundleId;
|
|
4439
4439
|
const isRecoveredFromBundle = (row, bundleId) => row.type === "RECOVERED" && row.from_bundle_id === bundleId;
|
|
4440
4440
|
const countDistinctInstallations = (rows) => new Set(rows.map(({ install_id }) => install_id)).size;
|
|
4441
|
-
const
|
|
4441
|
+
const getInsightsResult = async (persistence, bundleId, window, limit, offset) => {
|
|
4442
4442
|
const scope = {
|
|
4443
4443
|
persistence,
|
|
4444
4444
|
cutoffMs: Date.now()
|
|
@@ -4480,7 +4480,7 @@ const getAnalyticsResult = async (persistence, bundleId, window, limit, offset)
|
|
|
4480
4480
|
}
|
|
4481
4481
|
};
|
|
4482
4482
|
};
|
|
4483
|
-
const
|
|
4483
|
+
const getInsightsSummaries = async (persistence, bundleIds, window) => {
|
|
4484
4484
|
const normalizedBundleIds = [...new Set(bundleIds)];
|
|
4485
4485
|
if (normalizedBundleIds.length === 0) return [];
|
|
4486
4486
|
const requestedBundleIds = new Set(normalizedBundleIds);
|
|
@@ -4504,9 +4504,9 @@ const getAnalyticsSummaries = async (persistence, bundleIds, window) => {
|
|
|
4504
4504
|
recovered: recoveredByBundleId.get(bundleId)?.size ?? 0
|
|
4505
4505
|
}));
|
|
4506
4506
|
};
|
|
4507
|
-
const
|
|
4507
|
+
const createInsightsProvider = (persistence) => Object.freeze({
|
|
4508
4508
|
mode: "bounded",
|
|
4509
|
-
maxMatchingRows:
|
|
4509
|
+
maxMatchingRows: INSIGHTS_SCAN_MAX_ROWS,
|
|
4510
4510
|
async appendBundleEvent(input) {
|
|
4511
4511
|
await persistence.append(createBundleEventRow(input));
|
|
4512
4512
|
},
|
|
@@ -4521,10 +4521,10 @@ const createAnalyticsProvider = (persistence) => Object.freeze({
|
|
|
4521
4521
|
};
|
|
4522
4522
|
},
|
|
4523
4523
|
getBundleEventSummaries(bundleIds, window) {
|
|
4524
|
-
return
|
|
4524
|
+
return getInsightsSummaries(persistence, bundleIds, window);
|
|
4525
4525
|
},
|
|
4526
|
-
|
|
4527
|
-
return
|
|
4526
|
+
getBundleEventInsights(bundleId, window, limit, offset) {
|
|
4527
|
+
return getInsightsResult(persistence, bundleId, window, limit, offset);
|
|
4528
4528
|
},
|
|
4529
4529
|
async getBundleEventOverview() {
|
|
4530
4530
|
const rows = await materializeEventRows({
|
|
@@ -5106,7 +5106,7 @@ const sqlProviders = [
|
|
|
5106
5106
|
const noSqlProviders = ["mongodb"];
|
|
5107
5107
|
[...sqlProviders, ...noSqlProviders];
|
|
5108
5108
|
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" && "
|
|
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" && "insights" in plugin.models && typeof plugin.models.insights === "object" && plugin.models.insights !== null && "append" in plugin.models.insights && typeof plugin.models.insights.append === "function" && "scan" in plugin.models.insights && typeof plugin.models.insights.scan === "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
5110
|
}
|
|
5111
5111
|
//#endregion
|
|
5112
5112
|
//#region ../../packages/server/dist/storageAccess.mjs
|
|
@@ -5222,21 +5222,21 @@ function createHotUpdaterCore(options) {
|
|
|
5222
5222
|
readStorageText
|
|
5223
5223
|
});
|
|
5224
5224
|
const clientAccess = normalizeClientAccess(options.clientAccess);
|
|
5225
|
-
const
|
|
5225
|
+
const insights = createInsightsProvider({
|
|
5226
5226
|
async append(row) {
|
|
5227
5227
|
await assertSchemaReady();
|
|
5228
|
-
return plugin.models.
|
|
5228
|
+
return plugin.models.insights.append(row);
|
|
5229
5229
|
},
|
|
5230
5230
|
async scan(input) {
|
|
5231
5231
|
await assertSchemaReady();
|
|
5232
|
-
return plugin.models.
|
|
5232
|
+
return plugin.models.insights.scan(input);
|
|
5233
5233
|
}
|
|
5234
5234
|
});
|
|
5235
5235
|
const apiKeys = createApiKeyManagement({
|
|
5236
5236
|
apiKeys: plugin.models.apiKeys,
|
|
5237
5237
|
beforeOperation: assertSchemaReady
|
|
5238
5238
|
});
|
|
5239
|
-
const handlers = createHotUpdaterHandlers(core.api,
|
|
5239
|
+
const handlers = createHotUpdaterHandlers(core.api, insights, clientAccess.type === "api-key" ? {
|
|
5240
5240
|
authenticate: (request) => authenticateApiKey({
|
|
5241
5241
|
apiKeys: plugin.models.apiKeys,
|
|
5242
5242
|
beforeLookup: assertSchemaReady,
|
|
@@ -5247,7 +5247,7 @@ function createHotUpdaterCore(options) {
|
|
|
5247
5247
|
} : void 0, downloadStorageObject);
|
|
5248
5248
|
const api = Object.assign({
|
|
5249
5249
|
adapterName: adapterCapabilities.adapterName ?? core.adapterName,
|
|
5250
|
-
|
|
5250
|
+
insights,
|
|
5251
5251
|
apiKeys,
|
|
5252
5252
|
handlers
|
|
5253
5253
|
}, core.api);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hot-updater/firebase",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.0.0-rc.
|
|
4
|
+
"version": "1.0.0-rc.1",
|
|
5
5
|
"description": "React Native OTA solution for self-hosted",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
7
7
|
"types": "dist/index.d.cts",
|
|
@@ -43,10 +43,10 @@
|
|
|
43
43
|
],
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"hono": "4.12.34",
|
|
46
|
-
"@hot-updater/cli-tools": "1.0.0-rc.
|
|
46
|
+
"@hot-updater/cli-tools": "1.0.0-rc.1",
|
|
47
47
|
"@hot-updater/core": "1.0.0-rc.0",
|
|
48
|
-
"@hot-updater/
|
|
49
|
-
"@hot-updater/
|
|
48
|
+
"@hot-updater/server": "1.0.0-rc.1",
|
|
49
|
+
"@hot-updater/plugin-core": "1.0.0-rc.1"
|
|
50
50
|
},
|
|
51
51
|
"publishConfig": {
|
|
52
52
|
"access": "public"
|
|
@@ -61,8 +61,8 @@
|
|
|
61
61
|
"firebase-tools": "^13.32.0",
|
|
62
62
|
"fkill": "^9.0.0",
|
|
63
63
|
"mime": "^4.0.4",
|
|
64
|
-
"@hot-updater/mock": "1.0.0-rc.
|
|
65
|
-
"@hot-updater/test-utils": "1.0.0-rc.
|
|
64
|
+
"@hot-updater/mock": "1.0.0-rc.1",
|
|
65
|
+
"@hot-updater/test-utils": "1.0.0-rc.1"
|
|
66
66
|
},
|
|
67
67
|
"peerDependencies": {
|
|
68
68
|
"firebase-admin": "*",
|