@hot-updater/server 1.0.0-rc.2 → 1.0.0-rc.21
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/LICENSE +12 -14
- package/dist/adapters/drizzle.cjs +32 -75
- package/dist/adapters/drizzle.d.cts +13 -5
- package/dist/adapters/drizzle.d.mts +13 -5
- package/dist/adapters/drizzle.mjs +32 -77
- package/dist/adapters/drizzleExecutor.cjs +114 -0
- package/dist/adapters/drizzleExecutor.d.cts +9 -0
- package/dist/adapters/drizzleExecutor.d.mts +9 -0
- package/dist/adapters/drizzleExecutor.mjs +112 -0
- package/dist/adapters/kysely.cjs +22 -31
- package/dist/adapters/kysely.d.cts +10 -6
- package/dist/adapters/kysely.d.mts +10 -6
- package/dist/adapters/kysely.mjs +23 -33
- package/dist/adapters/kyselyExecutor.cjs +32 -0
- package/dist/adapters/kyselyExecutor.d.cts +11 -0
- package/dist/adapters/kyselyExecutor.d.mts +11 -0
- package/dist/adapters/kyselyExecutor.mjs +32 -0
- package/dist/adapters/mongodb.cjs +18 -54
- package/dist/adapters/mongodb.d.cts +10 -5
- package/dist/adapters/mongodb.d.mts +10 -5
- package/dist/adapters/mongodb.mjs +19 -56
- package/dist/adapters/mongodbAdapter.cjs +192 -0
- package/dist/adapters/mongodbAdapter.d.cts +9 -0
- package/dist/adapters/mongodbAdapter.d.mts +9 -0
- package/dist/adapters/mongodbAdapter.mjs +191 -0
- package/dist/adapters/prisma.cjs +35 -267
- package/dist/adapters/prisma.d.cts +12 -7
- package/dist/adapters/prisma.d.mts +12 -7
- package/dist/adapters/prisma.mjs +36 -268
- package/dist/adapters/prismaExecutor.cjs +63 -0
- package/dist/adapters/prismaExecutor.mjs +63 -0
- package/dist/adapters/sqlProviders.cjs +14 -0
- package/dist/adapters/sqlProviders.mjs +14 -0
- package/dist/assembly/assemblePlugins.cjs +144 -0
- package/dist/assembly/assemblePlugins.d.cts +5 -0
- package/dist/assembly/assemblePlugins.d.mts +5 -0
- package/dist/assembly/assemblePlugins.mjs +139 -0
- package/dist/assembly/clientPlugins.cjs +88 -0
- package/dist/assembly/clientPlugins.mjs +88 -0
- package/dist/assembly/configError.d.cts +2 -0
- package/dist/assembly/configError.d.mts +2 -0
- package/dist/assembly/configError.mjs +2 -0
- package/dist/core/api.cjs +51 -0
- package/dist/core/api.d.cts +54 -0
- package/dist/core/api.d.mts +54 -0
- package/dist/core/api.mjs +51 -0
- package/dist/core/baseCandidates.cjs +28 -0
- package/dist/core/baseCandidates.mjs +28 -0
- package/dist/core/operations.cjs +351 -0
- package/dist/core/operations.mjs +351 -0
- package/dist/core/reads.cjs +273 -0
- package/dist/core/reads.d.cts +9 -0
- package/dist/core/reads.d.mts +9 -0
- package/dist/core/reads.mjs +267 -0
- package/dist/core/releases.cjs +193 -0
- package/dist/core/releases.mjs +190 -0
- package/dist/core/writes.cjs +96 -0
- package/dist/core/writes.mjs +91 -0
- package/dist/createHotUpdaterCore.cjs +72 -91
- package/dist/createHotUpdaterCore.d.cts +106 -38
- package/dist/createHotUpdaterCore.d.mts +106 -38
- package/dist/createHotUpdaterCore.mjs +74 -89
- package/dist/db/engineDrizzleSchema.cjs +53 -0
- package/dist/db/engineDrizzleSchema.mjs +53 -0
- package/dist/db/enginePrismaSchema.cjs +84 -0
- package/dist/db/enginePrismaSchema.d.cts +5 -0
- package/dist/db/enginePrismaSchema.d.mts +5 -0
- package/dist/db/enginePrismaSchema.mjs +83 -0
- package/dist/db/releaseCatalog.cjs +24 -47
- package/dist/db/releaseCatalog.mjs +20 -44
- package/dist/db/updateArtifacts.cjs +43 -72
- package/dist/db/updateArtifacts.mjs +40 -69
- package/dist/handler.cjs +59 -43
- package/dist/handler.d.cts +3 -5
- package/dist/handler.d.mts +3 -5
- package/dist/handler.mjs +61 -43
- package/dist/handlerAdminRoutes.cjs +374 -0
- package/dist/handlerAdminRoutes.mjs +372 -0
- package/dist/handlerParameters.cjs +6 -11
- package/dist/handlerParameters.mjs +6 -10
- package/dist/handlerReleaseCatalogRoutes.cjs +32 -15
- package/dist/handlerReleaseCatalogRoutes.mjs +32 -15
- package/dist/handlerTypes.d.cts +2 -55
- package/dist/handlerTypes.d.mts +2 -55
- package/dist/handlerVersionRoutes.cjs +20 -4
- package/dist/handlerVersionRoutes.mjs +20 -4
- package/dist/index.cjs +9 -9
- package/dist/index.d.cts +8 -9
- package/dist/index.d.mts +8 -9
- package/dist/index.mjs +3 -4
- package/dist/node.cjs +3 -6
- package/dist/node.d.cts +2 -2
- package/dist/node.d.mts +2 -2
- package/dist/node.mjs +4 -4
- package/dist/package.cjs +1 -1
- package/dist/package.mjs +1 -1
- package/dist/plugins/api-keys/index.cjs +9 -0
- package/dist/plugins/api-keys/index.d.cts +1 -0
- package/dist/plugins/api-keys/index.d.mts +1 -0
- package/dist/plugins/api-keys/index.mjs +2 -0
- package/dist/plugins/definePlugin.d.cts +2 -0
- package/dist/plugins/definePlugin.d.mts +2 -0
- package/dist/plugins/insights/index.cjs +9 -0
- package/dist/plugins/insights/index.d.cts +1 -0
- package/dist/plugins/insights/index.d.mts +1 -0
- package/dist/plugins/insights/index.mjs +2 -0
- package/dist/plugins/official.cjs +30 -0
- package/dist/plugins/official.mjs +29 -0
- package/dist/storageAccess.cjs +13 -10
- package/dist/storageAccess.mjs +13 -10
- package/dist/types/index.d.cts +2 -28
- package/dist/types/index.d.mts +2 -28
- package/package.json +26 -14
- package/dist/_virtual/_rolldown/runtime.cjs +0 -23
- package/dist/adapters/databaseConstraintErrors.cjs +0 -22
- package/dist/adapters/databaseConstraintErrors.mjs +0 -21
- package/dist/adapters/databasePluginUtils.cjs +0 -116
- package/dist/adapters/databasePluginUtils.mjs +0 -106
- package/dist/adapters/drizzleCrud.cjs +0 -311
- package/dist/adapters/drizzleCrud.mjs +0 -308
- package/dist/adapters/drizzleLazyDB.cjs +0 -106
- package/dist/adapters/drizzleLazyDB.mjs +0 -106
- package/dist/adapters/drizzleQuery.cjs +0 -69
- package/dist/adapters/drizzleQuery.mjs +0 -69
- package/dist/adapters/insightsTransaction.cjs +0 -22
- package/dist/adapters/insightsTransaction.mjs +0 -22
- package/dist/adapters/kyselyCrud.cjs +0 -305
- package/dist/adapters/kyselyCrud.mjs +0 -304
- package/dist/adapters/kyselyQuery.cjs +0 -56
- package/dist/adapters/kyselyQuery.mjs +0 -56
- package/dist/adapters/mongodbCollections.cjs +0 -38
- package/dist/adapters/mongodbCollections.mjs +0 -32
- package/dist/adapters/mongodbQuery.cjs +0 -94
- package/dist/adapters/mongodbQuery.mjs +0 -86
- package/dist/adapters/mongodbReads.cjs +0 -210
- package/dist/adapters/mongodbReads.mjs +0 -210
- package/dist/adapters/mongodbWrites.cjs +0 -148
- package/dist/adapters/mongodbWrites.mjs +0 -148
- package/dist/adapters/prismaQuery.cjs +0 -57
- package/dist/adapters/prismaQuery.mjs +0 -56
- package/dist/adapters/prismaRows.cjs +0 -219
- package/dist/adapters/prismaRows.mjs +0 -209
- package/dist/apiKeys.cjs +0 -143
- package/dist/apiKeys.d.cts +0 -49
- package/dist/apiKeys.d.mts +0 -49
- package/dist/apiKeys.mjs +0 -133
- package/dist/db/createBundleDiff.cjs +0 -144
- package/dist/db/createBundleDiff.d.cts +0 -16
- package/dist/db/createBundleDiff.d.mts +0 -16
- package/dist/db/createBundleDiff.mjs +0 -141
- package/dist/db/databasePluginCore.cjs +0 -137
- package/dist/db/databasePluginCore.mjs +0 -137
- package/dist/db/fixedMigrator.cjs +0 -79
- package/dist/db/fixedMigrator.mjs +0 -78
- package/dist/db/fixedMigratorMongo.cjs +0 -321
- package/dist/db/fixedMigratorMongo.mjs +0 -321
- package/dist/db/fixedMigratorShared.cjs +0 -23
- package/dist/db/fixedMigratorShared.mjs +0 -18
- package/dist/db/index.cjs +0 -27
- package/dist/db/index.d.cts +0 -12
- package/dist/db/index.d.mts +0 -12
- package/dist/db/index.mjs +0 -22
- package/dist/db/mongoMigrationExecution.cjs +0 -11
- package/dist/db/mongoMigrationExecution.mjs +0 -11
- package/dist/db/releaseCatalog.d.cts +0 -16
- package/dist/db/releaseCatalog.d.mts +0 -16
- package/dist/db/schema/insightsCollation.cjs +0 -14
- package/dist/db/schema/insightsCollation.mjs +0 -14
- package/dist/db/schema/mongodb.cjs +0 -25
- package/dist/db/schema/mongodb.mjs +0 -25
- package/dist/db/schema/registry.cjs +0 -21
- package/dist/db/schema/registry.mjs +0 -17
- package/dist/db/schema/sql.cjs +0 -105
- package/dist/db/schema/sql.mjs +0 -97
- package/dist/db/schema/sqlOperations.cjs +0 -27
- package/dist/db/schema/sqlOperations.mjs +0 -26
- package/dist/db/schemaGenerators.cjs +0 -275
- package/dist/db/schemaGenerators.mjs +0 -273
- package/dist/db/schemaReadiness.cjs +0 -26
- package/dist/db/schemaReadiness.d.cts +0 -9
- package/dist/db/schemaReadiness.d.mts +0 -9
- package/dist/db/schemaReadiness.mjs +0 -25
- package/dist/db/sqlMigrationExecution.cjs +0 -67
- package/dist/db/sqlMigrationExecution.mjs +0 -67
- package/dist/db/types.cjs +0 -22
- package/dist/db/types.d.cts +0 -118
- package/dist/db/types.d.mts +0 -118
- package/dist/db/types.mjs +0 -19
- package/dist/handlerBundleRoutes.cjs +0 -155
- package/dist/handlerBundleRoutes.mjs +0 -155
- package/dist/handlerReleaseManagementRoutes.cjs +0 -124
- package/dist/handlerReleaseManagementRoutes.mjs +0 -124
- package/dist/insights/domain.d.cts +0 -89
- package/dist/insights/domain.d.mts +0 -89
- package/dist/insights/errors.cjs +0 -15
- package/dist/insights/errors.mjs +0 -14
- package/dist/insights/eventInput.cjs +0 -155
- package/dist/insights/eventInput.mjs +0 -153
- package/dist/insights/provider.cjs +0 -314
- package/dist/insights/provider.d.cts +0 -6
- package/dist/insights/provider.d.mts +0 -6
- package/dist/insights/provider.mjs +0 -314
- package/dist/insights/queryInput.cjs +0 -93
- package/dist/insights/queryInput.mjs +0 -91
- package/dist/insights/routes.cjs +0 -58
- package/dist/insights/routes.mjs +0 -56
- package/dist/insights/types.d.cts +0 -32
- package/dist/insights/types.d.mts +0 -32
- package/dist/schema/dsl.cjs +0 -103
- package/dist/schema/dsl.mjs +0 -87
- package/dist/schema/index.cjs +0 -20
- package/dist/schema/index.mjs +0 -7
- package/dist/schema/settings.cjs +0 -9
- package/dist/schema/settings.mjs +0 -9
- package/dist/schema/types.cjs +0 -8
- package/dist/schema/types.mjs +0 -6
- package/dist/schema/v1_0_0.cjs +0 -406
- package/dist/schema/v1_0_0.mjs +0 -398
- package/src/adapters/databaseConstraintErrors.ts +0 -25
- package/src/adapters/databasePluginTestDatabase.ts +0 -120
- package/src/adapters/databasePluginUtils.spec.ts +0 -29
- package/src/adapters/databasePluginUtils.ts +0 -214
- package/src/adapters/drizzle.spec.ts +0 -336
- package/src/adapters/drizzle.ts +0 -162
- package/src/adapters/drizzleCrud.ts +0 -571
- package/src/adapters/drizzleLazyDB.ts +0 -271
- package/src/adapters/drizzleQuery.ts +0 -164
- package/src/adapters/insightsTransaction.ts +0 -32
- package/src/adapters/kysely.spec.ts +0 -174
- package/src/adapters/kysely.ts +0 -110
- package/src/adapters/kyselyCrud.ts +0 -709
- package/src/adapters/kyselyQuery.ts +0 -128
- package/src/adapters/mongodb.integration.spec.ts +0 -210
- package/src/adapters/mongodb.spec.ts +0 -209
- package/src/adapters/mongodb.ts +0 -138
- package/src/adapters/mongodbCollections.ts +0 -67
- package/src/adapters/mongodbQuery.ts +0 -255
- package/src/adapters/mongodbReads.ts +0 -380
- package/src/adapters/mongodbRows.spec.ts +0 -64
- package/src/adapters/mongodbRows.ts +0 -118
- package/src/adapters/mongodbTestClient.ts +0 -377
- package/src/adapters/mongodbTestFilter.ts +0 -169
- package/src/adapters/mongodbWrites.ts +0 -287
- package/src/adapters/prisma.spec.ts +0 -293
- package/src/adapters/prisma.ts +0 -565
- package/src/adapters/prismaQuery.ts +0 -129
- package/src/adapters/prismaRows.spec.ts +0 -98
- package/src/adapters/prismaRows.ts +0 -337
- package/src/adapters/prismaTestClient.ts +0 -413
- package/src/apiKeys.integration.spec.ts +0 -192
- package/src/apiKeys.spec.ts +0 -162
- package/src/apiKeys.ts +0 -220
- package/src/calculatePagination.ts +0 -34
- package/src/createHotUpdaterCore.ts +0 -246
- package/src/db/bundleRows.spec.ts +0 -164
- package/src/db/bundleRows.ts +0 -10
- package/src/db/createBundleDiff.spec.ts +0 -450
- package/src/db/createBundleDiff.ts +0 -364
- package/src/db/databasePluginCore.spec.ts +0 -62
- package/src/db/databasePluginCore.testFixtures.ts +0 -76
- package/src/db/databasePluginCore.ts +0 -185
- package/src/db/databasePluginUpdateCheck.bench.ts +0 -110
- package/src/db/fixedMigrator.spec.ts +0 -296
- package/src/db/fixedMigrator.ts +0 -147
- package/src/db/fixedMigratorMongo.spec.ts +0 -239
- package/src/db/fixedMigratorMongo.ts +0 -425
- package/src/db/fixedMigratorShared.ts +0 -36
- package/src/db/hotUpdaterSchema.ts +0 -5
- package/src/db/index.spec.ts +0 -1269
- package/src/db/index.ts +0 -47
- package/src/db/mongoMigrationExecution.ts +0 -23
- package/src/db/node-sqlite.d.ts +0 -25
- package/src/db/releaseCatalog.ts +0 -148
- package/src/db/schema/definitions.ts +0 -1
- package/src/db/schema/insightsCollation.ts +0 -21
- package/src/db/schema/mongodb.ts +0 -40
- package/src/db/schema/registry.ts +0 -55
- package/src/db/schema/sql.ts +0 -244
- package/src/db/schema/sqlOperations.ts +0 -63
- package/src/db/schema/types.ts +0 -1
- package/src/db/schemaGenerators.ts +0 -494
- package/src/db/schemaReadiness.ts +0 -34
- package/src/db/sqlMigrationExecution.spec.ts +0 -65
- package/src/db/sqlMigrationExecution.ts +0 -108
- package/src/db/types.spec.ts +0 -63
- package/src/db/types.ts +0 -228
- package/src/db/updateArtifacts.spec.ts +0 -359
- package/src/db/updateArtifacts.ts +0 -541
- package/src/handler-standalone.integration.spec.ts +0 -528
- package/src/handler.spec.ts +0 -190
- package/src/handler.testFixtures.ts +0 -30
- package/src/handler.ts +0 -245
- package/src/handlerAuth.integration.spec.ts +0 -33
- package/src/handlerBundleRoutes.ts +0 -259
- package/src/handlerErrors.ts +0 -7
- package/src/handlerManagement.spec.ts +0 -262
- package/src/handlerParameters.ts +0 -105
- package/src/handlerReleaseCatalogRoutes.ts +0 -154
- package/src/handlerReleaseManagementRoutes.ts +0 -213
- package/src/handlerTypes.ts +0 -96
- package/src/handlerVersionRoutes.ts +0 -12
- package/src/index.ts +0 -30
- package/src/insights/domain.ts +0 -103
- package/src/insights/errors.ts +0 -11
- package/src/insights/eventInput.ts +0 -225
- package/src/insights/insights.integration.spec.ts +0 -381
- package/src/insights/provider.spec.ts +0 -385
- package/src/insights/provider.ts +0 -591
- package/src/insights/queryInput.ts +0 -141
- package/src/insights/routes.ts +0 -104
- package/src/insights/types.ts +0 -55
- package/src/internalRouter.ts +0 -104
- package/src/node.spec.ts +0 -119
- package/src/node.ts +0 -111
- package/src/releaseCatalogRoutes.integration.spec.ts +0 -238
- package/src/runtime.spec.ts +0 -194
- package/src/runtime.testFixtures.ts +0 -71
- package/src/schema/dsl-all-versions.spec.ts +0 -37
- package/src/schema/dsl.ts +0 -187
- package/src/schema/index.ts +0 -10
- package/src/schema/settings.ts +0 -15
- package/src/schema/types.ts +0 -79
- package/src/schema/v1_0_0.spec.ts +0 -220
- package/src/schema/v1_0_0.ts +0 -529
- package/src/storageAccess.spec.ts +0 -152
- package/src/storageAccess.ts +0 -141
- package/src/types/index.ts +0 -34
- package/src/version.ts +0 -3
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
import { createUUIDv7, type BundleEventRow } from "@hot-updater/plugin-core";
|
|
2
|
-
|
|
3
|
-
import type {
|
|
4
|
-
CreateBundleEventRequest,
|
|
5
|
-
CreateBundleEventRequestBase,
|
|
6
|
-
} from "./domain";
|
|
7
|
-
import {
|
|
8
|
-
InsightsBadRequestError,
|
|
9
|
-
InsightsPayloadTooLargeError,
|
|
10
|
-
} from "./errors";
|
|
11
|
-
|
|
12
|
-
const MAX_EVENT_STRING_LENGTH = 1_024;
|
|
13
|
-
const MAX_IDENTITY_LENGTH = 255;
|
|
14
|
-
export const EVENT_BODY_MAX_BYTES = 16 * 1_024;
|
|
15
|
-
|
|
16
|
-
const eventKeys = new Set([
|
|
17
|
-
"type",
|
|
18
|
-
"installId",
|
|
19
|
-
"toBundleId",
|
|
20
|
-
"userId",
|
|
21
|
-
"username",
|
|
22
|
-
"platform",
|
|
23
|
-
"appVersion",
|
|
24
|
-
"channel",
|
|
25
|
-
"cohort",
|
|
26
|
-
"fingerprintHash",
|
|
27
|
-
"fromBundleId",
|
|
28
|
-
"fromReleaseId",
|
|
29
|
-
"toReleaseId",
|
|
30
|
-
"updateStrategy",
|
|
31
|
-
"sdkVersion",
|
|
32
|
-
]);
|
|
33
|
-
|
|
34
|
-
function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
|
|
35
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function requireStringField(
|
|
39
|
-
payload: Readonly<Record<string, unknown>>,
|
|
40
|
-
key: string,
|
|
41
|
-
): string {
|
|
42
|
-
const value = payload[key];
|
|
43
|
-
if (
|
|
44
|
-
typeof value !== "string" ||
|
|
45
|
-
value.length === 0 ||
|
|
46
|
-
value.length > MAX_EVENT_STRING_LENGTH ||
|
|
47
|
-
new TextDecoder("utf-8", { ignoreBOM: true }).decode(
|
|
48
|
-
new TextEncoder().encode(value),
|
|
49
|
-
) !== value
|
|
50
|
-
) {
|
|
51
|
-
throw new InsightsBadRequestError(`Invalid event field: ${key}`);
|
|
52
|
-
}
|
|
53
|
-
return value;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function requireNullableStringField(
|
|
57
|
-
payload: Readonly<Record<string, unknown>>,
|
|
58
|
-
key: string,
|
|
59
|
-
): string | null {
|
|
60
|
-
if (payload[key] === null) return null;
|
|
61
|
-
return requireStringField(payload, key);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function requireIdentityField(
|
|
65
|
-
payload: Readonly<Record<string, unknown>>,
|
|
66
|
-
key: "installId" | "userId",
|
|
67
|
-
): string {
|
|
68
|
-
const value = requireStringField(payload, key);
|
|
69
|
-
if (value.length > MAX_IDENTITY_LENGTH) {
|
|
70
|
-
throw new InsightsBadRequestError(`Invalid event field: ${key}`);
|
|
71
|
-
}
|
|
72
|
-
return value;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async function readBoundedText(request: Request): Promise<string> {
|
|
76
|
-
const contentLength = request.headers.get("content-length");
|
|
77
|
-
const declaredByteLength = Number(contentLength);
|
|
78
|
-
if (
|
|
79
|
-
contentLength !== null &&
|
|
80
|
-
Number.isSafeInteger(declaredByteLength) &&
|
|
81
|
-
declaredByteLength > EVENT_BODY_MAX_BYTES
|
|
82
|
-
) {
|
|
83
|
-
throw new InsightsPayloadTooLargeError(EVENT_BODY_MAX_BYTES);
|
|
84
|
-
}
|
|
85
|
-
if (request.body === null) return "";
|
|
86
|
-
const reader = request.body.getReader();
|
|
87
|
-
const decoder = new TextDecoder();
|
|
88
|
-
let byteLength = 0;
|
|
89
|
-
let text = "";
|
|
90
|
-
while (true) {
|
|
91
|
-
const result = await reader.read();
|
|
92
|
-
if (result.done) break;
|
|
93
|
-
byteLength += result.value.byteLength;
|
|
94
|
-
if (byteLength > EVENT_BODY_MAX_BYTES) {
|
|
95
|
-
await reader.cancel();
|
|
96
|
-
throw new InsightsPayloadTooLargeError(EVENT_BODY_MAX_BYTES);
|
|
97
|
-
}
|
|
98
|
-
text += decoder.decode(result.value, { stream: true });
|
|
99
|
-
}
|
|
100
|
-
return text + decoder.decode();
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
async function parseJson(request: Request): Promise<unknown> {
|
|
104
|
-
const text = await readBoundedText(request);
|
|
105
|
-
try {
|
|
106
|
-
return JSON.parse(text);
|
|
107
|
-
} catch (error) {
|
|
108
|
-
if (error instanceof SyntaxError) {
|
|
109
|
-
throw new InsightsBadRequestError("Invalid event payload");
|
|
110
|
-
}
|
|
111
|
-
throw error;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function requireEvent(payload: unknown): CreateBundleEventRequest {
|
|
116
|
-
if (
|
|
117
|
-
!isRecord(payload) ||
|
|
118
|
-
Object.keys(payload).some((key) => !eventKeys.has(key))
|
|
119
|
-
) {
|
|
120
|
-
throw new InsightsBadRequestError("Invalid event payload");
|
|
121
|
-
}
|
|
122
|
-
const platform = requireStringField(payload, "platform");
|
|
123
|
-
if (platform !== "ios" && platform !== "android") {
|
|
124
|
-
throw new InsightsBadRequestError("Invalid event field: platform");
|
|
125
|
-
}
|
|
126
|
-
const base: CreateBundleEventRequestBase = {
|
|
127
|
-
installId: requireIdentityField(payload, "installId"),
|
|
128
|
-
toBundleId: requireStringField(payload, "toBundleId"),
|
|
129
|
-
...(payload.userId === undefined
|
|
130
|
-
? {}
|
|
131
|
-
: { userId: requireIdentityField(payload, "userId") }),
|
|
132
|
-
...(payload.username === undefined
|
|
133
|
-
? {}
|
|
134
|
-
: { username: requireStringField(payload, "username") }),
|
|
135
|
-
platform,
|
|
136
|
-
appVersion: requireStringField(payload, "appVersion"),
|
|
137
|
-
channel: requireStringField(payload, "channel"),
|
|
138
|
-
cohort: requireStringField(payload, "cohort"),
|
|
139
|
-
fingerprintHash: requireNullableStringField(payload, "fingerprintHash"),
|
|
140
|
-
sdkVersion:
|
|
141
|
-
payload.sdkVersion === undefined
|
|
142
|
-
? null
|
|
143
|
-
: requireNullableStringField(payload, "sdkVersion"),
|
|
144
|
-
fromReleaseId: requireNullableStringField(payload, "fromReleaseId"),
|
|
145
|
-
toReleaseId: requireNullableStringField(payload, "toReleaseId"),
|
|
146
|
-
};
|
|
147
|
-
const type = requireStringField(payload, "type");
|
|
148
|
-
switch (type) {
|
|
149
|
-
case "UPDATE_APPLIED":
|
|
150
|
-
case "RECOVERED":
|
|
151
|
-
case "RELEASE_ADOPTED": {
|
|
152
|
-
const updateStrategy = requireStringField(payload, "updateStrategy");
|
|
153
|
-
if (updateStrategy !== "fingerprint" && updateStrategy !== "appVersion") {
|
|
154
|
-
throw new InsightsBadRequestError(
|
|
155
|
-
"Invalid event field: updateStrategy",
|
|
156
|
-
);
|
|
157
|
-
}
|
|
158
|
-
return {
|
|
159
|
-
...base,
|
|
160
|
-
type,
|
|
161
|
-
fromBundleId: requireStringField(payload, "fromBundleId"),
|
|
162
|
-
updateStrategy,
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
case "UNCHANGED":
|
|
166
|
-
if (payload.fromBundleId !== null || payload.updateStrategy !== null) {
|
|
167
|
-
throw new InsightsBadRequestError("Invalid unchanged event shape");
|
|
168
|
-
}
|
|
169
|
-
return {
|
|
170
|
-
...base,
|
|
171
|
-
type,
|
|
172
|
-
fromBundleId: null,
|
|
173
|
-
updateStrategy: null,
|
|
174
|
-
};
|
|
175
|
-
default:
|
|
176
|
-
throw new InsightsBadRequestError("Invalid event field: type");
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
export async function parseBundleEventRequest(
|
|
181
|
-
request: Request,
|
|
182
|
-
): Promise<CreateBundleEventRequest> {
|
|
183
|
-
const payload = await parseJson(request);
|
|
184
|
-
return requireEvent(payload);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
export function createBundleEventRow(
|
|
188
|
-
input: CreateBundleEventRequest,
|
|
189
|
-
): BundleEventRow {
|
|
190
|
-
input = requireEvent(input);
|
|
191
|
-
const base = {
|
|
192
|
-
app_version: input.appVersion,
|
|
193
|
-
channel: input.channel,
|
|
194
|
-
cohort: input.cohort,
|
|
195
|
-
fingerprint_hash: input.fingerprintHash,
|
|
196
|
-
from_release_id: input.fromReleaseId,
|
|
197
|
-
id: createUUIDv7(),
|
|
198
|
-
install_id: input.installId,
|
|
199
|
-
platform: input.platform,
|
|
200
|
-
received_at_ms: Date.now(),
|
|
201
|
-
sdk_version: input.sdkVersion ?? null,
|
|
202
|
-
to_bundle_id: input.toBundleId,
|
|
203
|
-
to_release_id: input.toReleaseId,
|
|
204
|
-
user_id: input.userId ?? null,
|
|
205
|
-
username: input.username ?? null,
|
|
206
|
-
};
|
|
207
|
-
switch (input.type) {
|
|
208
|
-
case "UPDATE_APPLIED":
|
|
209
|
-
case "RECOVERED":
|
|
210
|
-
case "RELEASE_ADOPTED":
|
|
211
|
-
return {
|
|
212
|
-
...base,
|
|
213
|
-
from_bundle_id: input.fromBundleId,
|
|
214
|
-
type: input.type,
|
|
215
|
-
update_strategy: input.updateStrategy,
|
|
216
|
-
};
|
|
217
|
-
case "UNCHANGED":
|
|
218
|
-
return {
|
|
219
|
-
...base,
|
|
220
|
-
from_bundle_id: null,
|
|
221
|
-
type: input.type,
|
|
222
|
-
update_strategy: null,
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
}
|
|
@@ -1,381 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
|
|
3
|
-
import { createInMemoryDatabasePlugin } from "../../../test-utils/test/inMemoryDatabasePlugin";
|
|
4
|
-
import { createHotUpdater } from "../index";
|
|
5
|
-
import { createSchemaManagedDatabase } from "../runtime.testFixtures";
|
|
6
|
-
import { HOT_UPDATER_SCHEMA_VERSION } from "../schema/types";
|
|
7
|
-
|
|
8
|
-
const event = {
|
|
9
|
-
appVersion: "1.0.0",
|
|
10
|
-
channel: "production",
|
|
11
|
-
cohort: "default",
|
|
12
|
-
fingerprintHash: null,
|
|
13
|
-
fromBundleId: null,
|
|
14
|
-
fromReleaseId: null,
|
|
15
|
-
installId: "install-1",
|
|
16
|
-
platform: "ios",
|
|
17
|
-
toBundleId: "bundle-1",
|
|
18
|
-
toReleaseId: null,
|
|
19
|
-
type: "UNCHANGED",
|
|
20
|
-
updateStrategy: null,
|
|
21
|
-
userId: "user-1",
|
|
22
|
-
username: "Jane",
|
|
23
|
-
sdkVersion: "2.0.0",
|
|
24
|
-
} as const;
|
|
25
|
-
|
|
26
|
-
const eventRequest = (body: unknown = event) =>
|
|
27
|
-
new Request("https://example.com/events", {
|
|
28
|
-
method: "POST",
|
|
29
|
-
headers: { "content-type": "application/json" },
|
|
30
|
-
body: JSON.stringify(body),
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
afterEach(() => {
|
|
34
|
-
vi.useRealTimers();
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
describe("createHotUpdater Insights", () => {
|
|
38
|
-
it("persists an event and serves the lean Insights views", async () => {
|
|
39
|
-
vi.useFakeTimers();
|
|
40
|
-
vi.setSystemTime(new Date("2026-08-12T00:00:00.000Z"));
|
|
41
|
-
const receivedAtMs = Date.now();
|
|
42
|
-
const database = createInMemoryDatabasePlugin();
|
|
43
|
-
const append = vi.spyOn(database.models.insights, "record");
|
|
44
|
-
const hotUpdater = createHotUpdater({
|
|
45
|
-
database,
|
|
46
|
-
clientAccess: { type: "public" },
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
const ingestion = await hotUpdater.handlers.client(eventRequest());
|
|
50
|
-
vi.advanceTimersByTime(1);
|
|
51
|
-
const events = await hotUpdater.handlers.admin(
|
|
52
|
-
new Request("https://example.com/events"),
|
|
53
|
-
);
|
|
54
|
-
const installation = await hotUpdater.handlers.admin(
|
|
55
|
-
new Request("https://example.com/installations/install-1"),
|
|
56
|
-
);
|
|
57
|
-
const matches = await hotUpdater.handlers.admin(
|
|
58
|
-
new Request("https://example.com/installations?userId=user-1"),
|
|
59
|
-
);
|
|
60
|
-
const active = await hotUpdater.handlers.admin(
|
|
61
|
-
new Request(
|
|
62
|
-
"https://example.com/overview?platform=ios&channel=production&window=24h",
|
|
63
|
-
),
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
expect(ingestion.status).toBe(204);
|
|
67
|
-
expect(append).toHaveBeenCalledWith(
|
|
68
|
-
expect.objectContaining({
|
|
69
|
-
event: expect.objectContaining({
|
|
70
|
-
install_id: "install-1",
|
|
71
|
-
sdk_version: "2.0.0",
|
|
72
|
-
to_bundle_id: "bundle-1",
|
|
73
|
-
type: "UNCHANGED",
|
|
74
|
-
}),
|
|
75
|
-
}),
|
|
76
|
-
);
|
|
77
|
-
expect(events.status).toBe(200);
|
|
78
|
-
await expect(events.json()).resolves.toMatchObject({
|
|
79
|
-
data: [
|
|
80
|
-
{
|
|
81
|
-
id: expect.any(String),
|
|
82
|
-
installId: "install-1",
|
|
83
|
-
receivedAtMs,
|
|
84
|
-
type: "UNCHANGED",
|
|
85
|
-
userId: "user-1",
|
|
86
|
-
username: "Jane",
|
|
87
|
-
},
|
|
88
|
-
],
|
|
89
|
-
nextCursor: null,
|
|
90
|
-
});
|
|
91
|
-
expect(installation.status).toBe(200);
|
|
92
|
-
await expect(installation.json()).resolves.toMatchObject({
|
|
93
|
-
installId: "install-1",
|
|
94
|
-
latestStatus: "UNCHANGED",
|
|
95
|
-
userId: "user-1",
|
|
96
|
-
});
|
|
97
|
-
expect(matches.status).toBe(200);
|
|
98
|
-
await expect(matches.json()).resolves.toMatchObject({
|
|
99
|
-
data: [{ installId: "install-1", userId: "user-1" }],
|
|
100
|
-
nextCursor: null,
|
|
101
|
-
});
|
|
102
|
-
expect(active.status).toBe(200);
|
|
103
|
-
await expect(active.json()).resolves.toEqual({
|
|
104
|
-
platform: "ios",
|
|
105
|
-
channel: "production",
|
|
106
|
-
sinceMs: Date.now() - 24 * 60 * 60 * 1_000,
|
|
107
|
-
beforeReceivedAtMs: Date.now(),
|
|
108
|
-
reportingInstallations: { count: 1, measuredAtMs: Date.now() },
|
|
109
|
-
window: "24h",
|
|
110
|
-
});
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it("serves scoped bundle counts and the same half-open recovery drill-down", async () => {
|
|
114
|
-
vi.useFakeTimers();
|
|
115
|
-
const hotUpdater = createHotUpdater({
|
|
116
|
-
database: createInMemoryDatabasePlugin(),
|
|
117
|
-
clientAccess: { type: "public" },
|
|
118
|
-
});
|
|
119
|
-
const report = async (
|
|
120
|
-
receivedAtMs: number,
|
|
121
|
-
overrides: Record<string, unknown>,
|
|
122
|
-
) => {
|
|
123
|
-
vi.setSystemTime(receivedAtMs);
|
|
124
|
-
const response = await hotUpdater.handlers.client(
|
|
125
|
-
eventRequest({ ...event, ...overrides }),
|
|
126
|
-
);
|
|
127
|
-
expect(response.status).toBe(204);
|
|
128
|
-
};
|
|
129
|
-
await report(100, {
|
|
130
|
-
type: "UPDATE_APPLIED",
|
|
131
|
-
fromBundleId: "A",
|
|
132
|
-
toBundleId: "B",
|
|
133
|
-
updateStrategy: "appVersion",
|
|
134
|
-
});
|
|
135
|
-
await report(200, {
|
|
136
|
-
type: "RECOVERED",
|
|
137
|
-
fromBundleId: "B",
|
|
138
|
-
toBundleId: "A",
|
|
139
|
-
updateStrategy: "appVersion",
|
|
140
|
-
});
|
|
141
|
-
await report(300, {
|
|
142
|
-
installId: "install-2",
|
|
143
|
-
type: "RELEASE_ADOPTED",
|
|
144
|
-
fromBundleId: "B",
|
|
145
|
-
toBundleId: "B",
|
|
146
|
-
updateStrategy: "appVersion",
|
|
147
|
-
});
|
|
148
|
-
await report(400, {
|
|
149
|
-
installId: "install-3",
|
|
150
|
-
platform: "android",
|
|
151
|
-
toBundleId: "B",
|
|
152
|
-
});
|
|
153
|
-
vi.setSystemTime(500);
|
|
154
|
-
const overview = await hotUpdater.handlers.admin(
|
|
155
|
-
new Request(
|
|
156
|
-
"https://example.com/overview?platform=ios&channel=production&window=24h&bundleId=B",
|
|
157
|
-
),
|
|
158
|
-
);
|
|
159
|
-
expect(overview.status).toBe(200);
|
|
160
|
-
await expect(overview.json()).resolves.toMatchObject({
|
|
161
|
-
sinceMs: 0,
|
|
162
|
-
beforeReceivedAtMs: 500,
|
|
163
|
-
reportingInstallations: { count: 2 },
|
|
164
|
-
bundle: {
|
|
165
|
-
bundleId: "B",
|
|
166
|
-
reportingInstallations: { count: 1 },
|
|
167
|
-
appliedReports: { count: 1 },
|
|
168
|
-
recoveredReports: { count: 1 },
|
|
169
|
-
adoptedReports: { count: 1 },
|
|
170
|
-
},
|
|
171
|
-
});
|
|
172
|
-
const drilldown = await hotUpdater.handlers.admin(
|
|
173
|
-
new Request(
|
|
174
|
-
"https://example.com/events?platform=ios&channel=production&bundleId=B&outcome=recovered&sinceMs=200&beforeReceivedAtMs=300",
|
|
175
|
-
),
|
|
176
|
-
);
|
|
177
|
-
await expect(drilldown.json()).resolves.toMatchObject({
|
|
178
|
-
data: [
|
|
179
|
-
{
|
|
180
|
-
type: "RECOVERED",
|
|
181
|
-
fromBundleId: "B",
|
|
182
|
-
toBundleId: "A",
|
|
183
|
-
receivedAtMs: 200,
|
|
184
|
-
},
|
|
185
|
-
],
|
|
186
|
-
nextCursor: null,
|
|
187
|
-
});
|
|
188
|
-
const outside = await hotUpdater.handlers.admin(
|
|
189
|
-
new Request(
|
|
190
|
-
"https://example.com/events?platform=ios&channel=production&bundleId=B&outcome=recovered&sinceMs=0&beforeReceivedAtMs=200",
|
|
191
|
-
),
|
|
192
|
-
);
|
|
193
|
-
await expect(outside.json()).resolves.toMatchObject({
|
|
194
|
-
data: [],
|
|
195
|
-
nextCursor: null,
|
|
196
|
-
});
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it.each([
|
|
200
|
-
"/overview?window=24h",
|
|
201
|
-
"/overview?platform=ios&channel=production&platform=android",
|
|
202
|
-
"/events?bundleId=B",
|
|
203
|
-
"/events?platform=ios&channel=production&bundleId=B&outcome=UNCHANGED",
|
|
204
|
-
"/events?sinceMs=20&beforeReceivedAtMs=10",
|
|
205
|
-
"/installations/install-1/events?platform=ios&channel=production&bundleId=B&outcome=applied",
|
|
206
|
-
])("rejects ambiguous or invalid Insights query %s", async (path) => {
|
|
207
|
-
const hotUpdater = createHotUpdater({
|
|
208
|
-
database: createInMemoryDatabasePlugin(),
|
|
209
|
-
clientAccess: { type: "public" },
|
|
210
|
-
});
|
|
211
|
-
expect(
|
|
212
|
-
(
|
|
213
|
-
await hotUpdater.handlers.admin(
|
|
214
|
-
new Request(`https://example.com${path}`),
|
|
215
|
-
)
|
|
216
|
-
).status,
|
|
217
|
-
).toBe(400);
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
it("checks the official schema before Insights reads and writes", async () => {
|
|
221
|
-
const database = createSchemaManagedDatabase(
|
|
222
|
-
"kysely",
|
|
223
|
-
HOT_UPDATER_SCHEMA_VERSION,
|
|
224
|
-
);
|
|
225
|
-
const createMigrator = vi.spyOn(database, "createMigrator");
|
|
226
|
-
const hotUpdater = createHotUpdater({
|
|
227
|
-
database,
|
|
228
|
-
clientAccess: { type: "public" },
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
expect((await hotUpdater.handlers.client(eventRequest())).status).toBe(204);
|
|
232
|
-
await hotUpdater.insights.listEvents({});
|
|
233
|
-
|
|
234
|
-
expect(createMigrator).toHaveBeenCalledOnce();
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
it("keeps ingestion and queries on separate handler surfaces", async () => {
|
|
238
|
-
const hotUpdater = createHotUpdater({
|
|
239
|
-
database: createInMemoryDatabasePlugin(),
|
|
240
|
-
clientAccess: { type: "public" },
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
expect((await hotUpdater.handlers.client(eventRequest())).status).toBe(204);
|
|
244
|
-
const clientQuery = await hotUpdater.handlers.client(
|
|
245
|
-
new Request("https://example.com/events"),
|
|
246
|
-
);
|
|
247
|
-
const adminIngestion = await hotUpdater.handlers.admin(eventRequest());
|
|
248
|
-
const adminQuery = await hotUpdater.handlers.admin(
|
|
249
|
-
new Request("https://example.com/events"),
|
|
250
|
-
);
|
|
251
|
-
|
|
252
|
-
expect(clientQuery.status).toBe(404);
|
|
253
|
-
expect(adminIngestion.status).toBe(404);
|
|
254
|
-
expect(adminQuery.status).toBe(200);
|
|
255
|
-
expect(adminQuery.headers.get("cache-control")).toBe("private, no-store");
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
it("returns a stable client error for malformed event payloads", async () => {
|
|
259
|
-
const hotUpdater = createHotUpdater({
|
|
260
|
-
database: createInMemoryDatabasePlugin(),
|
|
261
|
-
clientAccess: { type: "public" },
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
const response = await hotUpdater.handlers.client(
|
|
265
|
-
eventRequest({ ...event, platform: "web" }),
|
|
266
|
-
);
|
|
267
|
-
|
|
268
|
-
expect(response.status).toBe(400);
|
|
269
|
-
await expect(response.json()).resolves.toEqual({
|
|
270
|
-
error: "Invalid event field: platform",
|
|
271
|
-
});
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
it.each(["installId", "userId"] as const)(
|
|
275
|
-
"rejects a 256-character event %s",
|
|
276
|
-
async (field) => {
|
|
277
|
-
const hotUpdater = createHotUpdater({
|
|
278
|
-
database: createInMemoryDatabasePlugin(),
|
|
279
|
-
clientAccess: { type: "public" },
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
const response = await hotUpdater.handlers.client(
|
|
283
|
-
eventRequest({ ...event, [field]: "x".repeat(256) }),
|
|
284
|
-
);
|
|
285
|
-
|
|
286
|
-
expect(response.status).toBe(400);
|
|
287
|
-
await expect(response.json()).resolves.toEqual({
|
|
288
|
-
error: `Invalid event field: ${field}`,
|
|
289
|
-
});
|
|
290
|
-
},
|
|
291
|
-
);
|
|
292
|
-
|
|
293
|
-
it("preserves a leading Unicode BOM in exact installation and current-user IDs", async () => {
|
|
294
|
-
const hotUpdater = createHotUpdater({
|
|
295
|
-
database: createInMemoryDatabasePlugin(),
|
|
296
|
-
clientAccess: { type: "public" },
|
|
297
|
-
});
|
|
298
|
-
const installId = "\uFEFFinstall";
|
|
299
|
-
const userId = "\uFEFFuser";
|
|
300
|
-
expect(
|
|
301
|
-
(
|
|
302
|
-
await hotUpdater.handlers.client(
|
|
303
|
-
eventRequest({ ...event, installId, userId }),
|
|
304
|
-
)
|
|
305
|
-
).status,
|
|
306
|
-
).toBe(204);
|
|
307
|
-
const installation = await hotUpdater.handlers.admin(
|
|
308
|
-
new Request(
|
|
309
|
-
`https://example.com/installations/${encodeURIComponent(installId)}`,
|
|
310
|
-
),
|
|
311
|
-
);
|
|
312
|
-
expect(installation.status).toBe(200);
|
|
313
|
-
await expect(installation.json()).resolves.toMatchObject({
|
|
314
|
-
installId,
|
|
315
|
-
userId,
|
|
316
|
-
});
|
|
317
|
-
const matches = await hotUpdater.handlers.admin(
|
|
318
|
-
new Request(
|
|
319
|
-
`https://example.com/installations?userId=${encodeURIComponent(userId)}`,
|
|
320
|
-
),
|
|
321
|
-
);
|
|
322
|
-
expect(matches.status).toBe(200);
|
|
323
|
-
await expect(matches.json()).resolves.toMatchObject({
|
|
324
|
-
data: [{ installId, userId }],
|
|
325
|
-
});
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
it.each(["\uD800", "\uDC00"])(
|
|
329
|
-
"rejects an unmatched surrogate identity %j",
|
|
330
|
-
async (installId) => {
|
|
331
|
-
const hotUpdater = createHotUpdater({
|
|
332
|
-
database: createInMemoryDatabasePlugin(),
|
|
333
|
-
clientAccess: { type: "public" },
|
|
334
|
-
});
|
|
335
|
-
expect(
|
|
336
|
-
(
|
|
337
|
-
await hotUpdater.handlers.client(
|
|
338
|
-
eventRequest({ ...event, installId }),
|
|
339
|
-
)
|
|
340
|
-
).status,
|
|
341
|
-
).toBe(400);
|
|
342
|
-
},
|
|
343
|
-
);
|
|
344
|
-
|
|
345
|
-
it("rejects 256-character installation query identities", async () => {
|
|
346
|
-
const hotUpdater = createHotUpdater({
|
|
347
|
-
database: createInMemoryDatabasePlugin(),
|
|
348
|
-
clientAccess: { type: "public" },
|
|
349
|
-
});
|
|
350
|
-
const tooLong = "x".repeat(256);
|
|
351
|
-
|
|
352
|
-
const user = await hotUpdater.handlers.admin(
|
|
353
|
-
new Request(`https://example.com/installations?userId=${tooLong}`),
|
|
354
|
-
);
|
|
355
|
-
const installation = await hotUpdater.handlers.admin(
|
|
356
|
-
new Request(`https://example.com/installations/${tooLong}`),
|
|
357
|
-
);
|
|
358
|
-
|
|
359
|
-
expect(user.status).toBe(400);
|
|
360
|
-
expect(installation.status).toBe(400);
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
it.each(["fromReleaseId", "toReleaseId"] as const)(
|
|
364
|
-
"rejects an omitted %s instead of treating it as null",
|
|
365
|
-
async (field) => {
|
|
366
|
-
const hotUpdater = createHotUpdater({
|
|
367
|
-
database: createInMemoryDatabasePlugin(),
|
|
368
|
-
clientAccess: { type: "public" },
|
|
369
|
-
});
|
|
370
|
-
const payload: Record<string, unknown> = { ...event };
|
|
371
|
-
delete payload[field];
|
|
372
|
-
|
|
373
|
-
const response = await hotUpdater.handlers.client(eventRequest(payload));
|
|
374
|
-
|
|
375
|
-
expect(response.status).toBe(400);
|
|
376
|
-
await expect(response.json()).resolves.toEqual({
|
|
377
|
-
error: `Invalid event field: ${field}`,
|
|
378
|
-
});
|
|
379
|
-
},
|
|
380
|
-
);
|
|
381
|
-
});
|