@hot-updater/server 1.0.0-rc.2 → 1.0.0-rc.3
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/adapters/prismaRows.cjs +2 -2
- package/dist/adapters/prismaRows.mjs +2 -2
- package/dist/insights/domain.d.cts +4 -4
- package/dist/insights/domain.d.mts +4 -4
- package/dist/insights/eventInput.cjs +2 -4
- package/dist/insights/eventInput.mjs +2 -4
- package/dist/insights/provider.cjs +5 -5
- package/dist/insights/provider.mjs +5 -5
- package/dist/insights/queryInput.cjs +1 -1
- package/dist/insights/queryInput.mjs +1 -1
- package/dist/package.cjs +1 -1
- package/dist/package.mjs +1 -1
- package/dist/schema/v1_0_0.cjs +3 -3
- package/dist/schema/v1_0_0.mjs +3 -3
- package/package.json +4 -4
- package/src/adapters/prismaRows.ts +2 -7
- package/src/insights/domain.ts +4 -8
- package/src/insights/eventInput.ts +1 -3
- package/src/insights/insights.integration.spec.ts +42 -4
- package/src/insights/provider.spec.ts +3 -3
- package/src/insights/provider.ts +5 -5
- package/src/insights/queryInput.ts +1 -1
- package/src/schema/v1_0_0.ts +3 -5
|
@@ -94,7 +94,7 @@ const parsePrismaBundleEventRow = (value) => {
|
|
|
94
94
|
const platform = readString(value, "platform");
|
|
95
95
|
const fromBundleId = readNullableString(value, "from_bundle_id");
|
|
96
96
|
const updateStrategy = readNullableString(value, "update_strategy");
|
|
97
|
-
if (platform !== "ios" && platform !== "android" || !((type === "UPDATE_APPLIED" || type === "RECOVERED"
|
|
97
|
+
if (platform !== "ios" && platform !== "android" || !((type === "UPDATE_APPLIED" || type === "RECOVERED") && typeof fromBundleId === "string" && (updateStrategy === "fingerprint" || updateStrategy === "appVersion") || type === "UNCHANGED" && fromBundleId === null && updateStrategy === null)) throw new PrismaAdapterError("invalid event shape");
|
|
98
98
|
return {
|
|
99
99
|
id: readString(value, "id"),
|
|
100
100
|
type,
|
|
@@ -119,7 +119,7 @@ const parsePrismaInsightsInstallationRow = (value) => {
|
|
|
119
119
|
if (!isRecord(value)) throw new PrismaAdapterError("invalid installation row");
|
|
120
120
|
const platform = readString(value, "platform");
|
|
121
121
|
const type = readString(value, "type");
|
|
122
|
-
if (platform !== "ios" && platform !== "android" || type !== "UPDATE_APPLIED" && type !== "RECOVERED" && type !== "
|
|
122
|
+
if (platform !== "ios" && platform !== "android" || type !== "UPDATE_APPLIED" && type !== "RECOVERED" && type !== "UNCHANGED") throw new PrismaAdapterError("invalid installation fields");
|
|
123
123
|
return {
|
|
124
124
|
install_id: readString(value, "install_id"),
|
|
125
125
|
id: readString(value, "id"),
|
|
@@ -94,7 +94,7 @@ const parsePrismaBundleEventRow = (value) => {
|
|
|
94
94
|
const platform = readString(value, "platform");
|
|
95
95
|
const fromBundleId = readNullableString(value, "from_bundle_id");
|
|
96
96
|
const updateStrategy = readNullableString(value, "update_strategy");
|
|
97
|
-
if (platform !== "ios" && platform !== "android" || !((type === "UPDATE_APPLIED" || type === "RECOVERED"
|
|
97
|
+
if (platform !== "ios" && platform !== "android" || !((type === "UPDATE_APPLIED" || type === "RECOVERED") && typeof fromBundleId === "string" && (updateStrategy === "fingerprint" || updateStrategy === "appVersion") || type === "UNCHANGED" && fromBundleId === null && updateStrategy === null)) throw new PrismaAdapterError("invalid event shape");
|
|
98
98
|
return {
|
|
99
99
|
id: readString(value, "id"),
|
|
100
100
|
type,
|
|
@@ -119,7 +119,7 @@ const parsePrismaInsightsInstallationRow = (value) => {
|
|
|
119
119
|
if (!isRecord(value)) throw new PrismaAdapterError("invalid installation row");
|
|
120
120
|
const platform = readString(value, "platform");
|
|
121
121
|
const type = readString(value, "type");
|
|
122
|
-
if (platform !== "ios" && platform !== "android" || type !== "UPDATE_APPLIED" && type !== "RECOVERED" && type !== "
|
|
122
|
+
if (platform !== "ios" && platform !== "android" || type !== "UPDATE_APPLIED" && type !== "RECOVERED" && type !== "UNCHANGED") throw new PrismaAdapterError("invalid installation fields");
|
|
123
123
|
return {
|
|
124
124
|
install_id: readString(value, "install_id"),
|
|
125
125
|
id: readString(value, "id"),
|
|
@@ -14,7 +14,7 @@ type CreateBundleEventRequestBase = {
|
|
|
14
14
|
readonly toReleaseId: string | null;
|
|
15
15
|
};
|
|
16
16
|
type CreateBundleEventRequest = (CreateBundleEventRequestBase & {
|
|
17
|
-
readonly type: "UPDATE_APPLIED" | "RECOVERED"
|
|
17
|
+
readonly type: "UPDATE_APPLIED" | "RECOVERED";
|
|
18
18
|
readonly fromBundleId: string;
|
|
19
19
|
readonly updateStrategy: "fingerprint" | "appVersion";
|
|
20
20
|
}) | (CreateBundleEventRequestBase & {
|
|
@@ -26,7 +26,7 @@ type ActiveInstallationWindow = "24h" | "7d" | "30d";
|
|
|
26
26
|
type EventHistoryRow = {
|
|
27
27
|
readonly id: string;
|
|
28
28
|
readonly installId: string;
|
|
29
|
-
readonly type: "UPDATE_APPLIED" | "RECOVERED" | "
|
|
29
|
+
readonly type: "UPDATE_APPLIED" | "RECOVERED" | "UNCHANGED";
|
|
30
30
|
readonly fromBundleId: string | null;
|
|
31
31
|
readonly toBundleId: string;
|
|
32
32
|
readonly username: string | null;
|
|
@@ -66,7 +66,7 @@ type InsightsScope = {
|
|
|
66
66
|
};
|
|
67
67
|
type InsightsBundleSelection = InsightsScope & {
|
|
68
68
|
readonly bundleId: string;
|
|
69
|
-
readonly outcome: "applied" | "recovered" | "
|
|
69
|
+
readonly outcome: "applied" | "recovered" | "unchanged";
|
|
70
70
|
};
|
|
71
71
|
type InsightsCountMeasurement = {
|
|
72
72
|
readonly count: number;
|
|
@@ -82,7 +82,7 @@ type ReportingOverview = InsightsScope & {
|
|
|
82
82
|
readonly reportingInstallations: InsightsCountMeasurement;
|
|
83
83
|
readonly appliedReports: InsightsCountMeasurement;
|
|
84
84
|
readonly recoveredReports: InsightsCountMeasurement;
|
|
85
|
-
readonly
|
|
85
|
+
readonly unchangedReports: InsightsCountMeasurement;
|
|
86
86
|
};
|
|
87
87
|
};
|
|
88
88
|
//#endregion
|
|
@@ -14,7 +14,7 @@ type CreateBundleEventRequestBase = {
|
|
|
14
14
|
readonly toReleaseId: string | null;
|
|
15
15
|
};
|
|
16
16
|
type CreateBundleEventRequest = (CreateBundleEventRequestBase & {
|
|
17
|
-
readonly type: "UPDATE_APPLIED" | "RECOVERED"
|
|
17
|
+
readonly type: "UPDATE_APPLIED" | "RECOVERED";
|
|
18
18
|
readonly fromBundleId: string;
|
|
19
19
|
readonly updateStrategy: "fingerprint" | "appVersion";
|
|
20
20
|
}) | (CreateBundleEventRequestBase & {
|
|
@@ -26,7 +26,7 @@ type ActiveInstallationWindow = "24h" | "7d" | "30d";
|
|
|
26
26
|
type EventHistoryRow = {
|
|
27
27
|
readonly id: string;
|
|
28
28
|
readonly installId: string;
|
|
29
|
-
readonly type: "UPDATE_APPLIED" | "RECOVERED" | "
|
|
29
|
+
readonly type: "UPDATE_APPLIED" | "RECOVERED" | "UNCHANGED";
|
|
30
30
|
readonly fromBundleId: string | null;
|
|
31
31
|
readonly toBundleId: string;
|
|
32
32
|
readonly username: string | null;
|
|
@@ -66,7 +66,7 @@ type InsightsScope = {
|
|
|
66
66
|
};
|
|
67
67
|
type InsightsBundleSelection = InsightsScope & {
|
|
68
68
|
readonly bundleId: string;
|
|
69
|
-
readonly outcome: "applied" | "recovered" | "
|
|
69
|
+
readonly outcome: "applied" | "recovered" | "unchanged";
|
|
70
70
|
};
|
|
71
71
|
type InsightsCountMeasurement = {
|
|
72
72
|
readonly count: number;
|
|
@@ -82,7 +82,7 @@ type ReportingOverview = InsightsScope & {
|
|
|
82
82
|
readonly reportingInstallations: InsightsCountMeasurement;
|
|
83
83
|
readonly appliedReports: InsightsCountMeasurement;
|
|
84
84
|
readonly recoveredReports: InsightsCountMeasurement;
|
|
85
|
-
readonly
|
|
85
|
+
readonly unchangedReports: InsightsCountMeasurement;
|
|
86
86
|
};
|
|
87
87
|
};
|
|
88
88
|
//#endregion
|
|
@@ -89,8 +89,7 @@ function requireEvent(payload) {
|
|
|
89
89
|
const type = requireStringField(payload, "type");
|
|
90
90
|
switch (type) {
|
|
91
91
|
case "UPDATE_APPLIED":
|
|
92
|
-
case "RECOVERED":
|
|
93
|
-
case "RELEASE_ADOPTED": {
|
|
92
|
+
case "RECOVERED": {
|
|
94
93
|
const updateStrategy = requireStringField(payload, "updateStrategy");
|
|
95
94
|
if (updateStrategy !== "fingerprint" && updateStrategy !== "appVersion") throw new require_errors.InsightsBadRequestError("Invalid event field: updateStrategy");
|
|
96
95
|
return {
|
|
@@ -134,8 +133,7 @@ function createBundleEventRow(input) {
|
|
|
134
133
|
};
|
|
135
134
|
switch (input.type) {
|
|
136
135
|
case "UPDATE_APPLIED":
|
|
137
|
-
case "RECOVERED":
|
|
138
|
-
case "RELEASE_ADOPTED": return {
|
|
136
|
+
case "RECOVERED": return {
|
|
139
137
|
...base,
|
|
140
138
|
from_bundle_id: input.fromBundleId,
|
|
141
139
|
type: input.type,
|
|
@@ -89,8 +89,7 @@ function requireEvent(payload) {
|
|
|
89
89
|
const type = requireStringField(payload, "type");
|
|
90
90
|
switch (type) {
|
|
91
91
|
case "UPDATE_APPLIED":
|
|
92
|
-
case "RECOVERED":
|
|
93
|
-
case "RELEASE_ADOPTED": {
|
|
92
|
+
case "RECOVERED": {
|
|
94
93
|
const updateStrategy = requireStringField(payload, "updateStrategy");
|
|
95
94
|
if (updateStrategy !== "fingerprint" && updateStrategy !== "appVersion") throw new InsightsBadRequestError("Invalid event field: updateStrategy");
|
|
96
95
|
return {
|
|
@@ -134,8 +133,7 @@ function createBundleEventRow(input) {
|
|
|
134
133
|
};
|
|
135
134
|
switch (input.type) {
|
|
136
135
|
case "UPDATE_APPLIED":
|
|
137
|
-
case "RECOVERED":
|
|
138
|
-
case "RELEASE_ADOPTED": return {
|
|
136
|
+
case "RECOVERED": return {
|
|
139
137
|
...base,
|
|
140
138
|
from_bundle_id: input.fromBundleId,
|
|
141
139
|
type: input.type,
|
|
@@ -74,9 +74,9 @@ const bundleFilter = (input) => {
|
|
|
74
74
|
type: "RECOVERED",
|
|
75
75
|
fromBundleId: bundleId
|
|
76
76
|
};
|
|
77
|
-
case "
|
|
77
|
+
case "unchanged": return {
|
|
78
78
|
...scope,
|
|
79
|
-
type: "
|
|
79
|
+
type: "UNCHANGED",
|
|
80
80
|
toBundleId: bundleId
|
|
81
81
|
};
|
|
82
82
|
default: throw new require_errors.InsightsBadRequestError("Invalid Insights outcome.");
|
|
@@ -283,7 +283,7 @@ const createInsightsProvider = (model) => Object.freeze({
|
|
|
283
283
|
sinceMs,
|
|
284
284
|
beforeReceivedAtMs
|
|
285
285
|
}));
|
|
286
|
-
const [reportingInstallations, bundleInstallations, appliedReports, recoveredReports,
|
|
286
|
+
const [reportingInstallations, bundleInstallations, appliedReports, recoveredReports, unchangedReports] = await Promise.all([
|
|
287
287
|
reporting,
|
|
288
288
|
measure(model.countInstallations({
|
|
289
289
|
...scope,
|
|
@@ -292,7 +292,7 @@ const createInsightsProvider = (model) => Object.freeze({
|
|
|
292
292
|
})),
|
|
293
293
|
countOutcome("applied"),
|
|
294
294
|
countOutcome("recovered"),
|
|
295
|
-
countOutcome("
|
|
295
|
+
countOutcome("unchanged")
|
|
296
296
|
]);
|
|
297
297
|
return {
|
|
298
298
|
...scope,
|
|
@@ -305,7 +305,7 @@ const createInsightsProvider = (model) => Object.freeze({
|
|
|
305
305
|
reportingInstallations: bundleInstallations,
|
|
306
306
|
appliedReports,
|
|
307
307
|
recoveredReports,
|
|
308
|
-
|
|
308
|
+
unchangedReports
|
|
309
309
|
}
|
|
310
310
|
};
|
|
311
311
|
}
|
|
@@ -74,9 +74,9 @@ const bundleFilter = (input) => {
|
|
|
74
74
|
type: "RECOVERED",
|
|
75
75
|
fromBundleId: bundleId
|
|
76
76
|
};
|
|
77
|
-
case "
|
|
77
|
+
case "unchanged": return {
|
|
78
78
|
...scope,
|
|
79
|
-
type: "
|
|
79
|
+
type: "UNCHANGED",
|
|
80
80
|
toBundleId: bundleId
|
|
81
81
|
};
|
|
82
82
|
default: throw new InsightsBadRequestError("Invalid Insights outcome.");
|
|
@@ -283,7 +283,7 @@ const createInsightsProvider = (model) => Object.freeze({
|
|
|
283
283
|
sinceMs,
|
|
284
284
|
beforeReceivedAtMs
|
|
285
285
|
}));
|
|
286
|
-
const [reportingInstallations, bundleInstallations, appliedReports, recoveredReports,
|
|
286
|
+
const [reportingInstallations, bundleInstallations, appliedReports, recoveredReports, unchangedReports] = await Promise.all([
|
|
287
287
|
reporting,
|
|
288
288
|
measure(model.countInstallations({
|
|
289
289
|
...scope,
|
|
@@ -292,7 +292,7 @@ const createInsightsProvider = (model) => Object.freeze({
|
|
|
292
292
|
})),
|
|
293
293
|
countOutcome("applied"),
|
|
294
294
|
countOutcome("recovered"),
|
|
295
|
-
countOutcome("
|
|
295
|
+
countOutcome("unchanged")
|
|
296
296
|
]);
|
|
297
297
|
return {
|
|
298
298
|
...scope,
|
|
@@ -305,7 +305,7 @@ const createInsightsProvider = (model) => Object.freeze({
|
|
|
305
305
|
reportingInstallations: bundleInstallations,
|
|
306
306
|
appliedReports,
|
|
307
307
|
recoveredReports,
|
|
308
|
-
|
|
308
|
+
unchangedReports
|
|
309
309
|
}
|
|
310
310
|
};
|
|
311
311
|
}
|
|
@@ -51,7 +51,7 @@ const parseEventPageInput = (request) => {
|
|
|
51
51
|
let bundle;
|
|
52
52
|
if (bundleFields.some((key) => url.searchParams.has(key))) {
|
|
53
53
|
const outcome = readSingle(url, "outcome");
|
|
54
|
-
if (outcome !== "applied" && outcome !== "recovered" && outcome !== "
|
|
54
|
+
if (outcome !== "applied" && outcome !== "recovered" && outcome !== "unchanged") throw new require_errors.InsightsBadRequestError("Invalid 'outcome' query parameter.");
|
|
55
55
|
bundle = {
|
|
56
56
|
...readScope(url),
|
|
57
57
|
bundleId: readId(url, "bundleId", 1024),
|
|
@@ -51,7 +51,7 @@ const parseEventPageInput = (request) => {
|
|
|
51
51
|
let bundle;
|
|
52
52
|
if (bundleFields.some((key) => url.searchParams.has(key))) {
|
|
53
53
|
const outcome = readSingle(url, "outcome");
|
|
54
|
-
if (outcome !== "applied" && outcome !== "recovered" && outcome !== "
|
|
54
|
+
if (outcome !== "applied" && outcome !== "recovered" && outcome !== "unchanged") throw new InsightsBadRequestError("Invalid 'outcome' query parameter.");
|
|
55
55
|
bundle = {
|
|
56
56
|
...readScope(url),
|
|
57
57
|
bundleId: readId(url, "bundleId", 1024),
|
package/dist/package.cjs
CHANGED
package/dist/package.mjs
CHANGED
package/dist/schema/v1_0_0.cjs
CHANGED
|
@@ -313,7 +313,7 @@ const bundleEventsV100 = require_dsl.table("bundle_events", {
|
|
|
313
313
|
checks: [
|
|
314
314
|
require_dsl.check({
|
|
315
315
|
name: "bundle_events_type_check",
|
|
316
|
-
expression: "type in ('UPDATE_APPLIED', 'RECOVERED', '
|
|
316
|
+
expression: "type in ('UPDATE_APPLIED', 'RECOVERED', 'UNCHANGED')",
|
|
317
317
|
sqliteInline: true
|
|
318
318
|
}),
|
|
319
319
|
require_dsl.check({
|
|
@@ -323,7 +323,7 @@ const bundleEventsV100 = require_dsl.table("bundle_events", {
|
|
|
323
323
|
}),
|
|
324
324
|
require_dsl.check({
|
|
325
325
|
name: "bundle_events_shape_check",
|
|
326
|
-
expression: "((type in ('UPDATE_APPLIED', 'RECOVERED'
|
|
326
|
+
expression: "((type in ('UPDATE_APPLIED', 'RECOVERED')) and from_bundle_id is not null and update_strategy is not null and update_strategy in ('fingerprint', 'appVersion')) or (type = 'UNCHANGED' and from_bundle_id is null and update_strategy is null)",
|
|
327
327
|
sqliteInline: true
|
|
328
328
|
}),
|
|
329
329
|
require_dsl.check({
|
|
@@ -364,7 +364,7 @@ const bundleInstallationsV100 = require_dsl.table("bundle_installations", {
|
|
|
364
364
|
checks: [
|
|
365
365
|
require_dsl.check({
|
|
366
366
|
name: "bundle_installations_type_check",
|
|
367
|
-
expression: "type in ('UPDATE_APPLIED', 'RECOVERED', '
|
|
367
|
+
expression: "type in ('UPDATE_APPLIED', 'RECOVERED', 'UNCHANGED')",
|
|
368
368
|
sqliteInline: true
|
|
369
369
|
}),
|
|
370
370
|
require_dsl.check({
|
package/dist/schema/v1_0_0.mjs
CHANGED
|
@@ -313,7 +313,7 @@ const bundleEventsV100 = table("bundle_events", {
|
|
|
313
313
|
checks: [
|
|
314
314
|
check({
|
|
315
315
|
name: "bundle_events_type_check",
|
|
316
|
-
expression: "type in ('UPDATE_APPLIED', 'RECOVERED', '
|
|
316
|
+
expression: "type in ('UPDATE_APPLIED', 'RECOVERED', 'UNCHANGED')",
|
|
317
317
|
sqliteInline: true
|
|
318
318
|
}),
|
|
319
319
|
check({
|
|
@@ -323,7 +323,7 @@ const bundleEventsV100 = table("bundle_events", {
|
|
|
323
323
|
}),
|
|
324
324
|
check({
|
|
325
325
|
name: "bundle_events_shape_check",
|
|
326
|
-
expression: "((type in ('UPDATE_APPLIED', 'RECOVERED'
|
|
326
|
+
expression: "((type in ('UPDATE_APPLIED', 'RECOVERED')) and from_bundle_id is not null and update_strategy is not null and update_strategy in ('fingerprint', 'appVersion')) or (type = 'UNCHANGED' and from_bundle_id is null and update_strategy is null)",
|
|
327
327
|
sqliteInline: true
|
|
328
328
|
}),
|
|
329
329
|
check({
|
|
@@ -364,7 +364,7 @@ const bundleInstallationsV100 = table("bundle_installations", {
|
|
|
364
364
|
checks: [
|
|
365
365
|
check({
|
|
366
366
|
name: "bundle_installations_type_check",
|
|
367
|
-
expression: "type in ('UPDATE_APPLIED', 'RECOVERED', '
|
|
367
|
+
expression: "type in ('UPDATE_APPLIED', 'RECOVERED', 'UNCHANGED')",
|
|
368
368
|
sqliteInline: true
|
|
369
369
|
}),
|
|
370
370
|
check({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hot-updater/server",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "React Native OTA solution for self-hosted",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"rou3": "0.7.9",
|
|
57
57
|
"verkit": "0.4.0",
|
|
58
58
|
"@hot-updater/core": "1.0.0-rc.0",
|
|
59
|
-
"@hot-updater/plugin-core": "1.0.0-rc.
|
|
59
|
+
"@hot-updater/plugin-core": "1.0.0-rc.3",
|
|
60
60
|
"@hot-updater/bsdiff": "1.0.0-rc.0"
|
|
61
61
|
},
|
|
62
62
|
"peerDependencies": {
|
|
@@ -91,8 +91,8 @@
|
|
|
91
91
|
"mongodb": "6.20.0",
|
|
92
92
|
"msw": "^2.7.0",
|
|
93
93
|
"uuidv7": "^1.0.2",
|
|
94
|
-
"@hot-updater/standalone": "1.0.0-rc.
|
|
95
|
-
"@hot-updater/test-utils": "1.0.0-rc.
|
|
94
|
+
"@hot-updater/standalone": "1.0.0-rc.3",
|
|
95
|
+
"@hot-updater/test-utils": "1.0.0-rc.3"
|
|
96
96
|
},
|
|
97
97
|
"scripts": {
|
|
98
98
|
"build": "tsdown",
|
|
@@ -168,9 +168,7 @@ export const parsePrismaBundleEventRow = (value: unknown): BundleEventRow => {
|
|
|
168
168
|
if (
|
|
169
169
|
(platform !== "ios" && platform !== "android") ||
|
|
170
170
|
!(
|
|
171
|
-
((type === "UPDATE_APPLIED" ||
|
|
172
|
-
type === "RECOVERED" ||
|
|
173
|
-
type === "RELEASE_ADOPTED") &&
|
|
171
|
+
((type === "UPDATE_APPLIED" || type === "RECOVERED") &&
|
|
174
172
|
typeof fromBundleId === "string" &&
|
|
175
173
|
(updateStrategy === "fingerprint" ||
|
|
176
174
|
updateStrategy === "appVersion")) ||
|
|
@@ -210,10 +208,7 @@ export const parsePrismaInsightsInstallationRow = (
|
|
|
210
208
|
const type = readString(value, "type");
|
|
211
209
|
if (
|
|
212
210
|
(platform !== "ios" && platform !== "android") ||
|
|
213
|
-
(type !== "UPDATE_APPLIED" &&
|
|
214
|
-
type !== "RECOVERED" &&
|
|
215
|
-
type !== "RELEASE_ADOPTED" &&
|
|
216
|
-
type !== "UNCHANGED")
|
|
211
|
+
(type !== "UPDATE_APPLIED" && type !== "RECOVERED" && type !== "UNCHANGED")
|
|
217
212
|
) {
|
|
218
213
|
throw new PrismaAdapterError("invalid installation fields");
|
|
219
214
|
}
|
package/src/insights/domain.ts
CHANGED
|
@@ -15,7 +15,7 @@ export type CreateBundleEventRequestBase = {
|
|
|
15
15
|
|
|
16
16
|
export type CreateBundleEventRequest =
|
|
17
17
|
| (CreateBundleEventRequestBase & {
|
|
18
|
-
readonly type: "UPDATE_APPLIED" | "RECOVERED"
|
|
18
|
+
readonly type: "UPDATE_APPLIED" | "RECOVERED";
|
|
19
19
|
readonly fromBundleId: string;
|
|
20
20
|
readonly updateStrategy: "fingerprint" | "appVersion";
|
|
21
21
|
})
|
|
@@ -30,11 +30,7 @@ export type ActiveInstallationWindow = "24h" | "7d" | "30d";
|
|
|
30
30
|
export type EventHistoryRow = {
|
|
31
31
|
readonly id: string;
|
|
32
32
|
readonly installId: string;
|
|
33
|
-
readonly type:
|
|
34
|
-
| "UPDATE_APPLIED"
|
|
35
|
-
| "RECOVERED"
|
|
36
|
-
| "RELEASE_ADOPTED"
|
|
37
|
-
| "UNCHANGED";
|
|
33
|
+
readonly type: "UPDATE_APPLIED" | "RECOVERED" | "UNCHANGED";
|
|
38
34
|
readonly fromBundleId: string | null;
|
|
39
35
|
readonly toBundleId: string;
|
|
40
36
|
readonly username: string | null;
|
|
@@ -80,7 +76,7 @@ export type InsightsScope = {
|
|
|
80
76
|
|
|
81
77
|
export type InsightsBundleSelection = InsightsScope & {
|
|
82
78
|
readonly bundleId: string;
|
|
83
|
-
readonly outcome: "applied" | "recovered" | "
|
|
79
|
+
readonly outcome: "applied" | "recovered" | "unchanged";
|
|
84
80
|
};
|
|
85
81
|
|
|
86
82
|
export type InsightsCountMeasurement = {
|
|
@@ -98,6 +94,6 @@ export type ReportingOverview = InsightsScope & {
|
|
|
98
94
|
readonly reportingInstallations: InsightsCountMeasurement;
|
|
99
95
|
readonly appliedReports: InsightsCountMeasurement;
|
|
100
96
|
readonly recoveredReports: InsightsCountMeasurement;
|
|
101
|
-
readonly
|
|
97
|
+
readonly unchangedReports: InsightsCountMeasurement;
|
|
102
98
|
};
|
|
103
99
|
};
|
|
@@ -147,8 +147,7 @@ function requireEvent(payload: unknown): CreateBundleEventRequest {
|
|
|
147
147
|
const type = requireStringField(payload, "type");
|
|
148
148
|
switch (type) {
|
|
149
149
|
case "UPDATE_APPLIED":
|
|
150
|
-
case "RECOVERED":
|
|
151
|
-
case "RELEASE_ADOPTED": {
|
|
150
|
+
case "RECOVERED": {
|
|
152
151
|
const updateStrategy = requireStringField(payload, "updateStrategy");
|
|
153
152
|
if (updateStrategy !== "fingerprint" && updateStrategy !== "appVersion") {
|
|
154
153
|
throw new InsightsBadRequestError(
|
|
@@ -207,7 +206,6 @@ export function createBundleEventRow(
|
|
|
207
206
|
switch (input.type) {
|
|
208
207
|
case "UPDATE_APPLIED":
|
|
209
208
|
case "RECOVERED":
|
|
210
|
-
case "RELEASE_ADOPTED":
|
|
211
209
|
return {
|
|
212
210
|
...base,
|
|
213
211
|
from_bundle_id: input.fromBundleId,
|
|
@@ -140,10 +140,10 @@ describe("createHotUpdater Insights", () => {
|
|
|
140
140
|
});
|
|
141
141
|
await report(300, {
|
|
142
142
|
installId: "install-2",
|
|
143
|
-
type: "
|
|
144
|
-
fromBundleId:
|
|
143
|
+
type: "UNCHANGED",
|
|
144
|
+
fromBundleId: null,
|
|
145
145
|
toBundleId: "B",
|
|
146
|
-
updateStrategy:
|
|
146
|
+
updateStrategy: null,
|
|
147
147
|
});
|
|
148
148
|
await report(400, {
|
|
149
149
|
installId: "install-3",
|
|
@@ -166,7 +166,7 @@ describe("createHotUpdater Insights", () => {
|
|
|
166
166
|
reportingInstallations: { count: 1 },
|
|
167
167
|
appliedReports: { count: 1 },
|
|
168
168
|
recoveredReports: { count: 1 },
|
|
169
|
-
|
|
169
|
+
unchangedReports: { count: 1 },
|
|
170
170
|
},
|
|
171
171
|
});
|
|
172
172
|
const drilldown = await hotUpdater.handlers.admin(
|
|
@@ -255,6 +255,44 @@ describe("createHotUpdater Insights", () => {
|
|
|
255
255
|
expect(adminQuery.headers.get("cache-control")).toBe("private, no-store");
|
|
256
256
|
});
|
|
257
257
|
|
|
258
|
+
it("records same-file selection as no change and rejects a fourth event type", async () => {
|
|
259
|
+
const database = createInMemoryDatabasePlugin();
|
|
260
|
+
const record = vi.spyOn(database.models.insights, "record");
|
|
261
|
+
const hotUpdater = createHotUpdater({
|
|
262
|
+
database,
|
|
263
|
+
clientAccess: { type: "public" },
|
|
264
|
+
});
|
|
265
|
+
const selection = {
|
|
266
|
+
...event,
|
|
267
|
+
fromReleaseId: "previous-release",
|
|
268
|
+
toReleaseId: "selected-release",
|
|
269
|
+
};
|
|
270
|
+
expect(
|
|
271
|
+
(await hotUpdater.handlers.client(eventRequest(selection))).status,
|
|
272
|
+
).toBe(204);
|
|
273
|
+
expect(record).toHaveBeenCalledWith({
|
|
274
|
+
event: expect.objectContaining({
|
|
275
|
+
type: "UNCHANGED",
|
|
276
|
+
from_bundle_id: null,
|
|
277
|
+
update_strategy: null,
|
|
278
|
+
from_release_id: "previous-release",
|
|
279
|
+
to_release_id: "selected-release",
|
|
280
|
+
to_bundle_id: event.toBundleId,
|
|
281
|
+
}),
|
|
282
|
+
installation: expect.objectContaining({ type: "UNCHANGED" }),
|
|
283
|
+
});
|
|
284
|
+
const response = await hotUpdater.handlers.client(
|
|
285
|
+
eventRequest({
|
|
286
|
+
...selection,
|
|
287
|
+
type: "RELEASE_ADOPTED",
|
|
288
|
+
fromBundleId: event.toBundleId,
|
|
289
|
+
updateStrategy: "appVersion",
|
|
290
|
+
}),
|
|
291
|
+
);
|
|
292
|
+
expect(response.status).toBe(400);
|
|
293
|
+
expect(record).toHaveBeenCalledTimes(1);
|
|
294
|
+
});
|
|
295
|
+
|
|
258
296
|
it("returns a stable client error for malformed event payloads", async () => {
|
|
259
297
|
const hotUpdater = createHotUpdater({
|
|
260
298
|
database: createInMemoryDatabasePlugin(),
|
|
@@ -13,7 +13,7 @@ const eventId = (index: number) =>
|
|
|
13
13
|
|
|
14
14
|
type TransitionEventRow = Extract<
|
|
15
15
|
BundleEventRow,
|
|
16
|
-
{ readonly type: "UPDATE_APPLIED" | "RECOVERED"
|
|
16
|
+
{ readonly type: "UPDATE_APPLIED" | "RECOVERED" }
|
|
17
17
|
>;
|
|
18
18
|
|
|
19
19
|
const eventRow = (
|
|
@@ -269,13 +269,13 @@ describe("createInsightsProvider", () => {
|
|
|
269
269
|
expect(result.bundle?.reportingInstallations.count).toBe(2);
|
|
270
270
|
expect(result.bundle?.appliedReports.count).toBe(5);
|
|
271
271
|
expect(result.bundle?.recoveredReports.count).toBe(3);
|
|
272
|
-
expect(result.bundle?.
|
|
272
|
+
expect(result.bundle?.unchangedReports.count).toBe(1);
|
|
273
273
|
expect(
|
|
274
274
|
fixture.countEvents.mock.calls.map(([input]) => input.filter),
|
|
275
275
|
).toEqual([
|
|
276
276
|
{ ...scope, type: "UPDATE_APPLIED", toBundleId: "B" },
|
|
277
277
|
{ ...scope, type: "RECOVERED", fromBundleId: "B" },
|
|
278
|
-
{ ...scope, type: "
|
|
278
|
+
{ ...scope, type: "UNCHANGED", toBundleId: "B" },
|
|
279
279
|
]);
|
|
280
280
|
await provider.listEvents({
|
|
281
281
|
bundle: { ...scope, bundleId: "B", outcome: "recovered" },
|
package/src/insights/provider.ts
CHANGED
|
@@ -157,8 +157,8 @@ const bundleFilter = (
|
|
|
157
157
|
return { ...scope, type: "UPDATE_APPLIED", toBundleId: bundleId };
|
|
158
158
|
case "recovered":
|
|
159
159
|
return { ...scope, type: "RECOVERED", fromBundleId: bundleId };
|
|
160
|
-
case "
|
|
161
|
-
return { ...scope, type: "
|
|
160
|
+
case "unchanged":
|
|
161
|
+
return { ...scope, type: "UNCHANGED", toBundleId: bundleId };
|
|
162
162
|
default:
|
|
163
163
|
throw new InsightsBadRequestError("Invalid Insights outcome.");
|
|
164
164
|
}
|
|
@@ -565,13 +565,13 @@ export const createInsightsProvider = (
|
|
|
565
565
|
bundleInstallations,
|
|
566
566
|
appliedReports,
|
|
567
567
|
recoveredReports,
|
|
568
|
-
|
|
568
|
+
unchangedReports,
|
|
569
569
|
] = await Promise.all([
|
|
570
570
|
reporting,
|
|
571
571
|
measure(model.countInstallations({ ...scope, sinceMs, bundleId })),
|
|
572
572
|
countOutcome("applied"),
|
|
573
573
|
countOutcome("recovered"),
|
|
574
|
-
countOutcome("
|
|
574
|
+
countOutcome("unchanged"),
|
|
575
575
|
]);
|
|
576
576
|
return {
|
|
577
577
|
...scope,
|
|
@@ -584,7 +584,7 @@ export const createInsightsProvider = (
|
|
|
584
584
|
reportingInstallations: bundleInstallations,
|
|
585
585
|
appliedReports,
|
|
586
586
|
recoveredReports,
|
|
587
|
-
|
|
587
|
+
unchangedReports,
|
|
588
588
|
},
|
|
589
589
|
};
|
|
590
590
|
},
|
package/src/schema/v1_0_0.ts
CHANGED
|
@@ -433,8 +433,7 @@ export const bundleEventsV100 = table(
|
|
|
433
433
|
checks: [
|
|
434
434
|
check({
|
|
435
435
|
name: "bundle_events_type_check",
|
|
436
|
-
expression:
|
|
437
|
-
"type in ('UPDATE_APPLIED', 'RECOVERED', 'RELEASE_ADOPTED', 'UNCHANGED')",
|
|
436
|
+
expression: "type in ('UPDATE_APPLIED', 'RECOVERED', 'UNCHANGED')",
|
|
438
437
|
sqliteInline: true,
|
|
439
438
|
}),
|
|
440
439
|
check({
|
|
@@ -445,7 +444,7 @@ export const bundleEventsV100 = table(
|
|
|
445
444
|
check({
|
|
446
445
|
name: "bundle_events_shape_check",
|
|
447
446
|
expression:
|
|
448
|
-
"((type in ('UPDATE_APPLIED', 'RECOVERED'
|
|
447
|
+
"((type in ('UPDATE_APPLIED', 'RECOVERED')) and from_bundle_id is not null and update_strategy is not null and update_strategy in ('fingerprint', 'appVersion')) or (type = 'UNCHANGED' and from_bundle_id is null and update_strategy is null)",
|
|
449
448
|
sqliteInline: true,
|
|
450
449
|
}),
|
|
451
450
|
check({
|
|
@@ -494,8 +493,7 @@ export const bundleInstallationsV100 = table(
|
|
|
494
493
|
checks: [
|
|
495
494
|
check({
|
|
496
495
|
name: "bundle_installations_type_check",
|
|
497
|
-
expression:
|
|
498
|
-
"type in ('UPDATE_APPLIED', 'RECOVERED', 'RELEASE_ADOPTED', 'UNCHANGED')",
|
|
496
|
+
expression: "type in ('UPDATE_APPLIED', 'RECOVERED', 'UNCHANGED')",
|
|
499
497
|
sqliteInline: true,
|
|
500
498
|
}),
|
|
501
499
|
check({
|