@hot-updater/server 1.0.0-rc.1 → 1.0.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_virtual/_rolldown/runtime.cjs +1 -1
- package/dist/adapters/databaseConstraintErrors.cjs +0 -1
- package/dist/adapters/databasePluginUtils.cjs +0 -1
- package/dist/adapters/drizzle.cjs +10 -3
- package/dist/adapters/drizzle.d.cts +0 -1
- package/dist/adapters/drizzle.d.mts +0 -1
- package/dist/adapters/drizzle.mjs +11 -3
- package/dist/adapters/drizzleCrud.cjs +62 -1
- package/dist/adapters/drizzleCrud.mjs +61 -2
- package/dist/adapters/drizzleLazyDB.cjs +6 -1
- package/dist/adapters/drizzleLazyDB.mjs +6 -1
- package/dist/adapters/drizzleQuery.cjs +3 -2
- package/dist/adapters/drizzleQuery.mjs +3 -1
- package/dist/adapters/insightsTransaction.cjs +22 -0
- package/dist/adapters/insightsTransaction.mjs +22 -0
- package/dist/adapters/kysely.cjs +2 -1
- package/dist/adapters/kysely.d.cts +0 -1
- package/dist/adapters/kysely.d.mts +0 -1
- package/dist/adapters/kysely.mjs +3 -1
- package/dist/adapters/kyselyCrud.cjs +54 -3
- package/dist/adapters/kyselyCrud.mjs +54 -3
- package/dist/adapters/kyselyQuery.cjs +2 -2
- package/dist/adapters/kyselyQuery.mjs +2 -1
- package/dist/adapters/mongodb.cjs +27 -1
- package/dist/adapters/mongodb.d.cts +1 -1
- package/dist/adapters/mongodb.d.mts +1 -1
- package/dist/adapters/mongodb.mjs +28 -1
- package/dist/adapters/mongodbCollections.cjs +2 -0
- package/dist/adapters/mongodbCollections.mjs +2 -0
- package/dist/adapters/mongodbQuery.cjs +21 -4
- package/dist/adapters/mongodbQuery.mjs +21 -5
- package/dist/adapters/mongodbReads.cjs +91 -29
- package/dist/adapters/mongodbReads.mjs +92 -29
- package/dist/adapters/mongodbWrites.cjs +16 -2
- package/dist/adapters/mongodbWrites.mjs +18 -3
- package/dist/adapters/prisma.cjs +55 -7
- package/dist/adapters/prisma.d.cts +0 -1
- package/dist/adapters/prisma.d.mts +0 -1
- package/dist/adapters/prisma.mjs +56 -7
- package/dist/adapters/prismaQuery.cjs +0 -1
- package/dist/adapters/prismaRows.cjs +22 -1
- package/dist/adapters/prismaRows.mjs +22 -1
- package/dist/apiKeys.cjs +6 -2
- package/dist/apiKeys.d.cts +0 -1
- package/dist/apiKeys.d.mts +0 -1
- package/dist/apiKeys.mjs +3 -3
- package/dist/createHotUpdaterCore.cjs +24 -9
- package/dist/createHotUpdaterCore.d.cts +1 -1
- package/dist/createHotUpdaterCore.d.mts +1 -1
- package/dist/createHotUpdaterCore.mjs +23 -9
- package/dist/db/createBundleDiff.cjs +2 -2
- package/dist/db/createBundleDiff.d.cts +1 -5
- package/dist/db/createBundleDiff.d.mts +1 -5
- package/dist/db/databasePluginCore.cjs +0 -1
- package/dist/db/fixedMigrator.cjs +2 -2
- package/dist/db/fixedMigrator.mjs +2 -2
- package/dist/db/fixedMigratorMongo.cjs +6 -4
- package/dist/db/fixedMigratorMongo.mjs +6 -4
- package/dist/db/index.d.cts +0 -1
- package/dist/db/index.d.mts +0 -1
- package/dist/db/releaseCatalog.cjs +0 -1
- package/dist/db/releaseCatalog.d.cts +0 -1
- package/dist/db/releaseCatalog.d.mts +0 -1
- package/dist/db/schema/insightsCollation.cjs +14 -0
- package/dist/db/schema/insightsCollation.mjs +14 -0
- package/dist/db/schema/sql.cjs +11 -7
- package/dist/db/schema/sql.mjs +8 -8
- package/dist/db/schemaGenerators.cjs +28 -9
- package/dist/db/schemaGenerators.mjs +28 -9
- package/dist/db/schemaReadiness.cjs +2 -0
- package/dist/db/schemaReadiness.d.cts +1 -0
- package/dist/db/schemaReadiness.d.mts +1 -0
- package/dist/db/schemaReadiness.mjs +2 -0
- package/dist/db/sqlMigrationExecution.cjs +1 -1
- package/dist/db/sqlMigrationExecution.mjs +1 -0
- package/dist/db/types.cjs +3 -1
- package/dist/db/types.d.cts +3 -6
- package/dist/db/types.d.mts +3 -6
- package/dist/db/types.mjs +2 -2
- package/dist/db/updateArtifacts.cjs +0 -1
- package/dist/handler.d.cts +1 -2
- package/dist/handler.d.mts +1 -2
- package/dist/handlerReleaseCatalogRoutes.cjs +0 -1
- package/dist/handlerReleaseManagementRoutes.cjs +0 -1
- package/dist/handlerTypes.d.cts +0 -1
- package/dist/handlerTypes.d.mts +0 -1
- package/dist/handlerVersionRoutes.d.cts +1 -0
- package/dist/handlerVersionRoutes.d.mts +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +4 -4
- package/dist/index.d.mts +4 -4
- package/dist/index.mjs +1 -1
- package/dist/insights/domain.d.cts +37 -69
- package/dist/insights/domain.d.mts +37 -69
- package/dist/insights/errors.cjs +1 -8
- package/dist/insights/errors.mjs +2 -8
- package/dist/insights/eventInput.cjs +49 -5
- package/dist/insights/eventInput.mjs +48 -6
- package/dist/insights/provider.cjs +314 -0
- package/dist/insights/provider.d.cts +6 -0
- package/dist/insights/provider.d.mts +6 -0
- package/dist/insights/provider.mjs +314 -0
- package/dist/insights/queryInput.cjs +78 -43
- package/dist/insights/queryInput.mjs +76 -40
- package/dist/insights/routes.cjs +20 -25
- package/dist/insights/routes.mjs +22 -27
- package/dist/insights/types.d.cts +27 -12
- package/dist/insights/types.d.mts +27 -12
- package/dist/node.d.cts +0 -1
- package/dist/node.d.mts +0 -1
- package/dist/package.cjs +1 -1
- package/dist/package.mjs +1 -1
- package/dist/schema/index.cjs +17 -3
- package/dist/schema/index.mjs +4 -4
- package/dist/schema/v1_0_0.cjs +72 -30
- package/dist/schema/v1_0_0.mjs +72 -31
- package/dist/storageAccess.cjs +0 -1
- package/dist/types/index.d.cts +0 -1
- package/dist/types/index.d.mts +0 -1
- package/package.json +7 -7
- package/src/adapters/databasePluginTestDatabase.ts +14 -0
- package/src/adapters/drizzle.spec.ts +99 -0
- package/src/adapters/drizzle.ts +21 -3
- package/src/adapters/drizzleCrud.ts +154 -2
- package/src/adapters/drizzleLazyDB.ts +17 -3
- package/src/adapters/insightsTransaction.ts +32 -0
- package/src/adapters/kysely.spec.ts +29 -0
- package/src/adapters/kysely.ts +10 -1
- package/src/adapters/kyselyCrud.ts +121 -0
- package/src/adapters/mongodb.integration.spec.ts +210 -0
- package/src/adapters/mongodb.spec.ts +69 -1
- package/src/adapters/mongodb.ts +66 -2
- package/src/adapters/mongodbCollections.ts +5 -0
- package/src/adapters/mongodbQuery.ts +32 -3
- package/src/adapters/mongodbReads.ts +69 -0
- package/src/adapters/mongodbTestClient.ts +41 -6
- package/src/adapters/mongodbTestFilter.ts +4 -1
- package/src/adapters/mongodbWrites.ts +27 -0
- package/src/adapters/prisma.spec.ts +97 -0
- package/src/adapters/prisma.ts +103 -3
- package/src/adapters/prismaRows.ts +37 -1
- package/src/adapters/prismaTestClient.ts +22 -5
- package/src/apiKeys.integration.spec.ts +1 -1
- package/src/createHotUpdaterCore.ts +21 -7
- package/src/db/databasePluginUpdateCheck.bench.ts +3 -0
- package/src/db/fixedMigrator.spec.ts +65 -0
- package/src/db/fixedMigratorMongo.spec.ts +56 -9
- package/src/db/fixedMigratorMongo.ts +10 -10
- package/src/db/index.spec.ts +8 -1
- package/src/db/schema/insightsCollation.ts +21 -0
- package/src/db/schema/sql.ts +8 -5
- package/src/db/schemaGenerators.ts +41 -13
- package/src/db/types.spec.ts +8 -0
- package/src/db/types.ts +10 -4
- package/src/handler-standalone.integration.spec.ts +5 -7
- package/src/index.ts +2 -2
- package/src/insights/domain.ts +39 -75
- package/src/insights/errors.ts +0 -7
- package/src/insights/eventInput.ts +60 -3
- package/src/insights/insights.integration.spec.ts +254 -17
- package/src/insights/provider.spec.ts +385 -0
- package/src/insights/provider.ts +591 -0
- package/src/insights/queryInput.ts +112 -73
- package/src/insights/routes.ts +37 -56
- package/src/insights/types.ts +47 -36
- package/src/schema/dsl-all-versions.spec.ts +3 -1
- package/src/schema/v1_0_0.spec.ts +77 -3
- package/src/schema/v1_0_0.ts +94 -30
- package/dist/insights/bounded/activeOverview.cjs +0 -87
- package/dist/insights/bounded/activeOverview.mjs +0 -85
- package/dist/insights/bounded/installationSearch.cjs +0 -42
- package/dist/insights/bounded/installationSearch.mjs +0 -42
- package/dist/insights/bounded/persistence.cjs +0 -39
- package/dist/insights/bounded/persistence.mjs +0 -38
- package/dist/insights/bounded/provider.cjs +0 -170
- package/dist/insights/bounded/provider.d.cts +0 -7
- package/dist/insights/bounded/provider.d.mts +0 -7
- package/dist/insights/bounded/provider.mjs +0 -170
- package/dist/insights/bounded/scan.cjs +0 -144
- package/dist/insights/bounded/scan.mjs +0 -138
- package/dist/insights/persistence.d.cts +0 -6
- package/dist/insights/persistence.d.mts +0 -6
- package/src/insights/bounded/activeOverview.spec.ts +0 -189
- package/src/insights/bounded/activeOverview.ts +0 -148
- package/src/insights/bounded/installationSearch.spec.ts +0 -130
- package/src/insights/bounded/installationSearch.ts +0 -69
- package/src/insights/bounded/persistence.ts +0 -43
- package/src/insights/bounded/provider.spec.ts +0 -219
- package/src/insights/bounded/provider.ts +0 -229
- package/src/insights/bounded/scan.ts +0 -235
- package/src/insights/persistence.ts +0 -10
|
@@ -1,102 +1,141 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
ActiveInstallationWindow,
|
|
3
|
+
InsightsBundleSelection,
|
|
4
|
+
InsightsScope,
|
|
4
5
|
} from "./domain";
|
|
5
6
|
import { InsightsBadRequestError } from "./errors";
|
|
7
|
+
import type {
|
|
8
|
+
InsightsEventPageInput,
|
|
9
|
+
InsightsUserInstallationPageInput,
|
|
10
|
+
} from "./types";
|
|
6
11
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
readonly maximum?: number;
|
|
11
|
-
};
|
|
12
|
+
const MAX_PAGE_LIMIT = 100;
|
|
13
|
+
const MAX_IDENTITY_LENGTH = 255;
|
|
14
|
+
const MAX_CURSOR_LENGTH = 8 * 1_024;
|
|
12
15
|
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
minimum: 0,
|
|
21
|
-
} as const satisfies IntegerBounds;
|
|
22
|
-
const MAX_USER_ID_LENGTH = 1_024;
|
|
16
|
+
const readSingle = (url: URL, key: string): string | undefined => {
|
|
17
|
+
const values = url.searchParams.getAll(key);
|
|
18
|
+
if (values.length > 1) {
|
|
19
|
+
throw new InsightsBadRequestError(`Duplicate '${key}' query parameter.`);
|
|
20
|
+
}
|
|
21
|
+
return values[0];
|
|
22
|
+
};
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
const readPageLimit = (url: URL): number | undefined => {
|
|
25
|
+
const value = readSingle(url, "limit");
|
|
26
|
+
if (value === undefined) return undefined;
|
|
27
|
+
const limit = Number(value);
|
|
28
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_PAGE_LIMIT) {
|
|
29
|
+
throw new InsightsBadRequestError("Invalid 'limit' query parameter.");
|
|
30
|
+
}
|
|
31
|
+
return limit;
|
|
27
32
|
};
|
|
28
33
|
|
|
29
|
-
|
|
30
|
-
|
|
34
|
+
const readCursor = (url: URL): string | undefined => {
|
|
35
|
+
const cursor = readSingle(url, "cursor");
|
|
36
|
+
if (
|
|
37
|
+
cursor !== undefined &&
|
|
38
|
+
(cursor.length === 0 || cursor.length > MAX_CURSOR_LENGTH)
|
|
39
|
+
) {
|
|
40
|
+
throw new InsightsBadRequestError("Invalid 'cursor' query parameter.");
|
|
41
|
+
}
|
|
42
|
+
return cursor;
|
|
31
43
|
};
|
|
32
44
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
45
|
+
const readId = (
|
|
46
|
+
url: URL,
|
|
47
|
+
key: string,
|
|
48
|
+
maximumLength = MAX_IDENTITY_LENGTH,
|
|
49
|
+
): string => {
|
|
50
|
+
const value = readSingle(url, key);
|
|
37
51
|
if (
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
52
|
+
value === undefined ||
|
|
53
|
+
value.length === 0 ||
|
|
54
|
+
value.length > maximumLength
|
|
41
55
|
) {
|
|
42
56
|
throw new InsightsBadRequestError(`Invalid '${key}' query parameter.`);
|
|
43
57
|
}
|
|
44
|
-
return
|
|
45
|
-
}
|
|
58
|
+
return value;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const readTimestamp = (url: URL, key: string): number | undefined => {
|
|
62
|
+
const raw = readSingle(url, key);
|
|
63
|
+
if (raw === undefined) return undefined;
|
|
64
|
+
const value = Number(raw);
|
|
65
|
+
if (!raw.length || !Number.isSafeInteger(value) || value < 0) {
|
|
66
|
+
throw new InsightsBadRequestError(`Invalid '${key}' query parameter.`);
|
|
67
|
+
}
|
|
68
|
+
return value;
|
|
69
|
+
};
|
|
46
70
|
|
|
47
|
-
|
|
71
|
+
const readScope = (url: URL): InsightsScope => {
|
|
72
|
+
const platform = readSingle(url, "platform");
|
|
73
|
+
if (platform !== "ios" && platform !== "android") {
|
|
74
|
+
throw new InsightsBadRequestError("Invalid 'platform' query parameter.");
|
|
75
|
+
}
|
|
76
|
+
return { platform, channel: readId(url, "channel", 1_024) };
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const parseEventPageInput = (
|
|
80
|
+
request: Request,
|
|
81
|
+
): InsightsEventPageInput => {
|
|
48
82
|
const url = new URL(request.url);
|
|
83
|
+
const bundleFields = ["bundleId", "outcome", "platform", "channel"];
|
|
84
|
+
let bundle: InsightsBundleSelection | undefined;
|
|
85
|
+
if (bundleFields.some((key) => url.searchParams.has(key))) {
|
|
86
|
+
const outcome = readSingle(url, "outcome");
|
|
87
|
+
if (
|
|
88
|
+
outcome !== "applied" &&
|
|
89
|
+
outcome !== "recovered" &&
|
|
90
|
+
outcome !== "adopted"
|
|
91
|
+
) {
|
|
92
|
+
throw new InsightsBadRequestError("Invalid 'outcome' query parameter.");
|
|
93
|
+
}
|
|
94
|
+
bundle = {
|
|
95
|
+
...readScope(url),
|
|
96
|
+
bundleId: readId(url, "bundleId", 1_024),
|
|
97
|
+
outcome,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
49
100
|
return {
|
|
50
|
-
|
|
51
|
-
|
|
101
|
+
beforeReceivedAtMs: readTimestamp(url, "beforeReceivedAtMs"),
|
|
102
|
+
sinceMs: readTimestamp(url, "sinceMs"),
|
|
103
|
+
cursor: readCursor(url),
|
|
104
|
+
limit: readPageLimit(url),
|
|
105
|
+
...(bundle === undefined ? {} : { bundle }),
|
|
52
106
|
};
|
|
53
107
|
};
|
|
54
108
|
|
|
55
|
-
export const
|
|
109
|
+
export const parseUserInstallationPageInput = (
|
|
110
|
+
request: Request,
|
|
111
|
+
): InsightsUserInstallationPageInput => {
|
|
56
112
|
const url = new URL(request.url);
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
throw new InsightsBadRequestError("Invalid 'window' query parameter.");
|
|
65
|
-
}
|
|
66
|
-
return { ...parsePagination(request), window };
|
|
113
|
+
const cursor = readCursor(url);
|
|
114
|
+
const limit = readPageLimit(url);
|
|
115
|
+
return {
|
|
116
|
+
userId: readId(url, "userId"),
|
|
117
|
+
...(cursor === undefined ? {} : { cursor }),
|
|
118
|
+
...(limit === undefined ? {} : { limit }),
|
|
119
|
+
};
|
|
67
120
|
};
|
|
68
121
|
|
|
69
|
-
export const
|
|
122
|
+
export const parseReportingOverviewInput = (
|
|
70
123
|
request: Request,
|
|
71
|
-
):
|
|
124
|
+
): InsightsScope & {
|
|
125
|
+
readonly window: ActiveInstallationWindow;
|
|
126
|
+
readonly bundleId?: string;
|
|
127
|
+
} => {
|
|
72
128
|
const url = new URL(request.url);
|
|
73
|
-
const
|
|
74
|
-
if (windows.length > 1) {
|
|
75
|
-
throw new InsightsBadRequestError("Duplicate 'window' query parameter.");
|
|
76
|
-
}
|
|
77
|
-
const window = windows[0] ?? "30d";
|
|
129
|
+
const window = readSingle(url, "window") ?? "30d";
|
|
78
130
|
if (window !== "24h" && window !== "7d" && window !== "30d") {
|
|
79
131
|
throw new InsightsBadRequestError("Invalid 'window' query parameter.");
|
|
80
132
|
}
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
) {
|
|
90
|
-
throw new InsightsBadRequestError("Invalid 'userId' query parameter.");
|
|
91
|
-
}
|
|
92
|
-
return userId === undefined ? { window } : { window, userId };
|
|
133
|
+
const bundleId = readSingle(url, "bundleId");
|
|
134
|
+
return {
|
|
135
|
+
...readScope(url),
|
|
136
|
+
window,
|
|
137
|
+
...(bundleId === undefined
|
|
138
|
+
? {}
|
|
139
|
+
: { bundleId: readId(url, "bundleId", 1_024) }),
|
|
140
|
+
};
|
|
93
141
|
};
|
|
94
|
-
|
|
95
|
-
export const parseSearchInput = (
|
|
96
|
-
request: Request,
|
|
97
|
-
): PaginationInput & { readonly query: string } => ({
|
|
98
|
-
...parsePagination(request),
|
|
99
|
-
query: new URL(request.url).searchParams.get("query")?.trim() ?? "",
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
export const parseEmptyInput = (): undefined => undefined;
|
package/src/insights/routes.ts
CHANGED
|
@@ -2,14 +2,12 @@ import type { RouteHandler } from "../handlerTypes";
|
|
|
2
2
|
import {
|
|
3
3
|
InsightsBadRequestError,
|
|
4
4
|
InsightsPayloadTooLargeError,
|
|
5
|
-
InsightsScanLimitExceededError,
|
|
6
5
|
} from "./errors";
|
|
7
6
|
import { parseBundleEventRequest } from "./eventInput";
|
|
8
7
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
parseSearchInput,
|
|
8
|
+
parseReportingOverviewInput,
|
|
9
|
+
parseEventPageInput,
|
|
10
|
+
parseUserInstallationPageInput,
|
|
13
11
|
} from "./queryInput";
|
|
14
12
|
import type { InsightsProvider } from "./types";
|
|
15
13
|
|
|
@@ -27,7 +25,11 @@ const requireParam = (
|
|
|
27
25
|
if (value === undefined || value.length === 0) {
|
|
28
26
|
throw new InsightsBadRequestError(`Missing route parameter: ${key}`);
|
|
29
27
|
}
|
|
30
|
-
|
|
28
|
+
try {
|
|
29
|
+
return decodeURIComponent(value);
|
|
30
|
+
} catch {
|
|
31
|
+
throw new InsightsBadRequestError(`Invalid route parameter: ${key}`);
|
|
32
|
+
}
|
|
31
33
|
};
|
|
32
34
|
|
|
33
35
|
const run = async (operation: () => Promise<Response>): Promise<Response> => {
|
|
@@ -40,17 +42,6 @@ const run = async (operation: () => Promise<Response>): Promise<Response> => {
|
|
|
40
42
|
if (error instanceof InsightsPayloadTooLargeError) {
|
|
41
43
|
return json({ error: error.message }, 413);
|
|
42
44
|
}
|
|
43
|
-
if (error instanceof InsightsScanLimitExceededError) {
|
|
44
|
-
return json(
|
|
45
|
-
{
|
|
46
|
-
error: {
|
|
47
|
-
code: "INSIGHTS_SCAN_LIMIT_EXCEEDED",
|
|
48
|
-
limit: error.limit,
|
|
49
|
-
},
|
|
50
|
-
},
|
|
51
|
-
503,
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
45
|
throw error;
|
|
55
46
|
}
|
|
56
47
|
};
|
|
@@ -66,43 +57,34 @@ export const createInsightsRouteHandlers = (
|
|
|
66
57
|
await provider.appendBundleEvent(await parseBundleEventRequest(request));
|
|
67
58
|
return new Response(null, { status: 204 });
|
|
68
59
|
}),
|
|
69
|
-
|
|
70
|
-
query(() =>
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
60
|
+
getReportingOverview: (_params, request) =>
|
|
61
|
+
query(() =>
|
|
62
|
+
provider.getReportingOverview(parseReportingOverviewInput(request)),
|
|
63
|
+
),
|
|
64
|
+
getInstallation: (params) =>
|
|
65
|
+
run(async () => {
|
|
66
|
+
const installation = await provider.getInstallation({
|
|
67
|
+
installId: requireParam(params, "installId"),
|
|
68
|
+
});
|
|
69
|
+
return installation === null
|
|
70
|
+
? json({ error: "Installation not found" }, 404)
|
|
71
|
+
: json(installation, 200);
|
|
80
72
|
}),
|
|
81
|
-
|
|
82
|
-
|
|
73
|
+
listEvents: (_params, request) =>
|
|
74
|
+
query(() => provider.listEvents(parseEventPageInput(request))),
|
|
75
|
+
listInstallationEvents: (params, request) =>
|
|
76
|
+
query(() =>
|
|
77
|
+
provider.listInstallationEvents({
|
|
78
|
+
...parseEventPageInput(request),
|
|
79
|
+
installId: requireParam(params, "installId"),
|
|
80
|
+
}),
|
|
81
|
+
),
|
|
82
|
+
pageInstallationsByCurrentUserId: (_params, request) =>
|
|
83
83
|
query(() =>
|
|
84
|
-
provider.
|
|
85
|
-
|
|
84
|
+
provider.pageInstallationsByCurrentUserId(
|
|
85
|
+
parseUserInstallationPageInput(request),
|
|
86
86
|
),
|
|
87
87
|
),
|
|
88
|
-
searchInstallations: (_params, request) =>
|
|
89
|
-
query(() => {
|
|
90
|
-
const input = parseSearchInput(request);
|
|
91
|
-
return provider.searchInstallations(
|
|
92
|
-
input.query,
|
|
93
|
-
input.limit,
|
|
94
|
-
input.offset,
|
|
95
|
-
);
|
|
96
|
-
}),
|
|
97
|
-
getInstallationHistory: (params, request) =>
|
|
98
|
-
query(() => {
|
|
99
|
-
const input = parsePagination(request);
|
|
100
|
-
return provider.getInstallationHistory(
|
|
101
|
-
requireParam(params, "installId"),
|
|
102
|
-
input.limit,
|
|
103
|
-
input.offset,
|
|
104
|
-
);
|
|
105
|
-
}),
|
|
106
88
|
});
|
|
107
89
|
|
|
108
90
|
export const registerInsightsClientRoutes = (
|
|
@@ -114,10 +96,9 @@ export const registerInsightsClientRoutes = (
|
|
|
114
96
|
export const registerInsightsAdminRoutes = (
|
|
115
97
|
add: (method: string, path: string, handler: string) => void,
|
|
116
98
|
): void => {
|
|
117
|
-
add("GET", "/
|
|
118
|
-
add("GET", "/
|
|
119
|
-
add("GET", "/installations
|
|
120
|
-
add("GET", "/installations/
|
|
121
|
-
add("GET", "/installations", "
|
|
122
|
-
add("GET", "/installations/:installId/events", "getInstallationHistory");
|
|
99
|
+
add("GET", "/events", "listEvents");
|
|
100
|
+
add("GET", "/overview", "getReportingOverview");
|
|
101
|
+
add("GET", "/installations", "pageInstallationsByCurrentUserId");
|
|
102
|
+
add("GET", "/installations/:installId/events", "listInstallationEvents");
|
|
103
|
+
add("GET", "/installations/:installId", "getInstallation");
|
|
123
104
|
};
|
package/src/insights/types.ts
CHANGED
|
@@ -1,44 +1,55 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
BundleEventOverview,
|
|
7
|
-
BundleEventSummary,
|
|
8
|
-
BundleEventSummaryByBundle,
|
|
2
|
+
ReportingOverview,
|
|
3
|
+
InsightsBundleSelection,
|
|
4
|
+
InsightsScope,
|
|
5
|
+
ActiveInstallationWindow,
|
|
9
6
|
CreateBundleEventRequest,
|
|
7
|
+
CursorPage,
|
|
8
|
+
EventCursorPage,
|
|
9
|
+
EventHistoryRow,
|
|
10
10
|
InstallationHistoryRow,
|
|
11
|
-
|
|
12
|
-
OffsetPaginationResult,
|
|
11
|
+
InstallationRow,
|
|
13
12
|
} from "./domain";
|
|
14
13
|
|
|
14
|
+
export type InsightsEventPageInput = {
|
|
15
|
+
readonly beforeReceivedAtMs?: number;
|
|
16
|
+
readonly sinceMs?: number;
|
|
17
|
+
readonly bundle?: InsightsBundleSelection;
|
|
18
|
+
readonly cursor?: string;
|
|
19
|
+
readonly limit?: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type InsightsInstallationEventPageInput = Omit<
|
|
23
|
+
InsightsEventPageInput,
|
|
24
|
+
"bundle"
|
|
25
|
+
> & {
|
|
26
|
+
readonly installId: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type InsightsUserInstallationPageInput = {
|
|
30
|
+
readonly userId: string;
|
|
31
|
+
readonly cursor?: string;
|
|
32
|
+
readonly limit?: number;
|
|
33
|
+
};
|
|
34
|
+
|
|
15
35
|
export type InsightsProvider = {
|
|
16
|
-
readonly mode: "bounded";
|
|
17
|
-
readonly maxMatchingRows: number;
|
|
18
36
|
appendBundleEvent(input: CreateBundleEventRequest): Promise<void>;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
input:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
offset: number,
|
|
38
|
-
): Promise<OffsetPaginationResult<InstallationSearchRow>>;
|
|
39
|
-
getInstallationHistory(
|
|
40
|
-
installId: string,
|
|
41
|
-
limit: number,
|
|
42
|
-
offset: number,
|
|
43
|
-
): Promise<OffsetPaginationResult<InstallationHistoryRow>>;
|
|
37
|
+
listEvents(
|
|
38
|
+
input: InsightsEventPageInput,
|
|
39
|
+
): Promise<EventCursorPage<EventHistoryRow>>;
|
|
40
|
+
listInstallationEvents(
|
|
41
|
+
input: InsightsInstallationEventPageInput,
|
|
42
|
+
): Promise<EventCursorPage<InstallationHistoryRow>>;
|
|
43
|
+
getInstallation(input: {
|
|
44
|
+
readonly installId: string;
|
|
45
|
+
}): Promise<InstallationRow | null>;
|
|
46
|
+
pageInstallationsByCurrentUserId(
|
|
47
|
+
input: InsightsUserInstallationPageInput,
|
|
48
|
+
): Promise<CursorPage<InstallationRow>>;
|
|
49
|
+
getReportingOverview(
|
|
50
|
+
input: InsightsScope & {
|
|
51
|
+
readonly window: ActiveInstallationWindow;
|
|
52
|
+
readonly bundleId?: string;
|
|
53
|
+
},
|
|
54
|
+
): Promise<ReportingOverview>;
|
|
44
55
|
};
|
|
@@ -4,6 +4,7 @@ import { hotUpdaterSchemaVersions } from "./index";
|
|
|
4
4
|
import { createSettingsTable } from "./settings";
|
|
5
5
|
import {
|
|
6
6
|
bundlePatchesV100,
|
|
7
|
+
bundleInstallationsV100,
|
|
7
8
|
bundlesV100,
|
|
8
9
|
channelsV100,
|
|
9
10
|
releaseCatalogsV100,
|
|
@@ -17,6 +18,7 @@ describe("versioned schema DSL", () => {
|
|
|
17
18
|
expect(channelsV100.dsl).toBe("table");
|
|
18
19
|
expect(bundlesV100.dsl).toBe("table");
|
|
19
20
|
expect(bundlePatchesV100.dsl).toBe("table");
|
|
21
|
+
expect(bundleInstallationsV100.dsl).toBe("table");
|
|
20
22
|
expect(releasesV100.dsl).toBe("table");
|
|
21
23
|
expect(releaseCatalogsV100.dsl).toBe("table");
|
|
22
24
|
expect(createSettingsTable("1.0.0").dsl).toBe("table");
|
|
@@ -27,7 +29,7 @@ describe("versioned schema DSL", () => {
|
|
|
27
29
|
}
|
|
28
30
|
});
|
|
29
31
|
|
|
30
|
-
it("registers only schema 1.0.0", () => {
|
|
32
|
+
it("registers only the initial schema 1.0.0", () => {
|
|
31
33
|
expect(hotUpdaterSchemaVersions.map((item) => item.version)).toEqual([
|
|
32
34
|
"1.0.0",
|
|
33
35
|
]);
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from "../db/schemaGenerators";
|
|
8
8
|
import {
|
|
9
9
|
bundleEventsV100,
|
|
10
|
+
bundleInstallationsV100,
|
|
10
11
|
bundlePatchesV100,
|
|
11
12
|
bundlesV100,
|
|
12
13
|
releaseCatalogsV100,
|
|
@@ -57,7 +58,7 @@ describe("v1.0.0 Release Catalog schema", () => {
|
|
|
57
58
|
const patchSize = bundlePatchesV100.columns.find(
|
|
58
59
|
({ ormName }) => ormName === "byte_size",
|
|
59
60
|
);
|
|
60
|
-
const sql = createTableSql("postgresql").join("\n");
|
|
61
|
+
const sql = createTableSql("postgresql", "foreign-keys", v1_0_0).join("\n");
|
|
61
62
|
const prisma = generatePrismaSchema("postgresql", v1_0_0);
|
|
62
63
|
const drizzle = generateDrizzleSchema("postgresql", v1_0_0);
|
|
63
64
|
|
|
@@ -96,6 +97,79 @@ describe("v1.0.0 Release Catalog schema", () => {
|
|
|
96
97
|
).toBeUndefined();
|
|
97
98
|
});
|
|
98
99
|
|
|
100
|
+
it("stores one current row per installation with the required lookup indexes", () => {
|
|
101
|
+
expect(
|
|
102
|
+
bundleInstallationsV100.columns.find(
|
|
103
|
+
({ ormName }) => ormName === "install_id",
|
|
104
|
+
)?.primaryKey,
|
|
105
|
+
).toBe(true);
|
|
106
|
+
expect(bundleInstallationsV100.indexes).toEqual(
|
|
107
|
+
expect.arrayContaining([
|
|
108
|
+
{
|
|
109
|
+
columns: ["user_id", "install_id"],
|
|
110
|
+
name: "bundle_installations_user_id_idx",
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
columns: ["received_at_ms"],
|
|
114
|
+
name: "bundle_installations_received_at_idx",
|
|
115
|
+
},
|
|
116
|
+
]),
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const sql = createTableSql("postgresql", "foreign-keys", v1_0_0).join("\n");
|
|
120
|
+
const prisma = generatePrismaSchema("postgresql", v1_0_0);
|
|
121
|
+
const drizzle = generateDrizzleSchema("postgresql", v1_0_0);
|
|
122
|
+
expect(sql).toContain("create table bundle_installations");
|
|
123
|
+
expect(sql).toContain(
|
|
124
|
+
'install_id varchar(255) collate "C" primary key not null',
|
|
125
|
+
);
|
|
126
|
+
expect(sql).toContain(
|
|
127
|
+
"create index bundle_installations_user_id_idx on bundle_installations(user_id, install_id)",
|
|
128
|
+
);
|
|
129
|
+
expect(sql).toContain(
|
|
130
|
+
"create index bundle_installations_received_at_idx on bundle_installations(received_at_ms)",
|
|
131
|
+
);
|
|
132
|
+
expect(sql).toContain(
|
|
133
|
+
"create index bundle_installations_bundle_idx on bundle_installations(platform, channel, to_bundle_id, received_at_ms)",
|
|
134
|
+
);
|
|
135
|
+
expect(sql).toContain(
|
|
136
|
+
"create index bundle_events_from_bundle_idx on bundle_events(type, platform, channel, from_bundle_id, received_at_ms, id)",
|
|
137
|
+
);
|
|
138
|
+
expect(prisma).toContain("initial Prisma SQL migration");
|
|
139
|
+
expect(prisma).toContain(
|
|
140
|
+
'alter table bundle_installations alter column install_id type varchar(255) collate "C"',
|
|
141
|
+
);
|
|
142
|
+
expect(prisma).toContain("model bundle_installations {");
|
|
143
|
+
expect(prisma).toContain("install_id String @db.VarChar(255) @id");
|
|
144
|
+
expect(drizzle).toContain("export const bundle_installations = pgTable(");
|
|
145
|
+
expect(drizzle).toContain(
|
|
146
|
+
'index("bundle_installations_user_id_idx").on(table.user_id, table.install_id)',
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("keeps identity indexes valid on MySQL and MSSQL", () => {
|
|
151
|
+
const mysql = createTableSql("mysql", "foreign-keys", v1_0_0).join("\n");
|
|
152
|
+
const mssql = createTableSql("mssql", "foreign-keys", v1_0_0).join("\n");
|
|
153
|
+
|
|
154
|
+
expect(mysql).toContain(
|
|
155
|
+
"install_id varchar(255) character set utf8mb4 collate utf8mb4_0900_bin not null",
|
|
156
|
+
);
|
|
157
|
+
expect(mysql).toContain("user_id varchar(255)");
|
|
158
|
+
expect(mssql).toContain("install_id nvarchar(255) not null");
|
|
159
|
+
expect(mssql).toContain("user_id nvarchar(255)");
|
|
160
|
+
|
|
161
|
+
for (const sql of [mysql, mssql]) {
|
|
162
|
+
expect(sql).toContain(
|
|
163
|
+
"create index bundle_events_install_idx on bundle_events(install_id, type, received_at_ms, id)",
|
|
164
|
+
);
|
|
165
|
+
expect(sql).toContain(
|
|
166
|
+
"create index bundle_installations_user_id_idx on bundle_installations(user_id, install_id)",
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
expect(mssql).not.toContain("install_id nvarchar(max)");
|
|
170
|
+
expect(mssql).not.toContain("user_id nvarchar(max)");
|
|
171
|
+
});
|
|
172
|
+
|
|
99
173
|
it("generates nullable source and artifact relations with the intended deletion rules", () => {
|
|
100
174
|
const prisma = generatePrismaSchema("postgresql", v1_0_0);
|
|
101
175
|
const drizzle = generateDrizzleSchema("postgresql", v1_0_0);
|
|
@@ -111,7 +185,7 @@ describe("v1.0.0 Release Catalog schema", () => {
|
|
|
111
185
|
});
|
|
112
186
|
|
|
113
187
|
it("creates both projection tables and their constraints from empty", () => {
|
|
114
|
-
const sql = createTableSql("postgresql").join("\n");
|
|
188
|
+
const sql = createTableSql("postgresql", "foreign-keys", v1_0_0).join("\n");
|
|
115
189
|
|
|
116
190
|
expect(sql).toContain("create table releases");
|
|
117
191
|
expect(sql).toContain("create table release_catalogs");
|
|
@@ -124,7 +198,7 @@ describe("v1.0.0 Release Catalog schema", () => {
|
|
|
124
198
|
});
|
|
125
199
|
|
|
126
200
|
it("uses byte-bounded MySQL catalog keys", () => {
|
|
127
|
-
const sql = createTableSql("mysql").join("\n");
|
|
201
|
+
const sql = createTableSql("mysql", "foreign-keys", v1_0_0).join("\n");
|
|
128
202
|
|
|
129
203
|
expect(sql).toContain(
|
|
130
204
|
"scope_key varchar(2048) character set ascii collate ascii_bin",
|