@hot-updater/server 1.0.0-rc.0 → 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.
Files changed (210) hide show
  1. package/dist/_virtual/_rolldown/runtime.cjs +1 -1
  2. package/dist/adapters/databaseConstraintErrors.cjs +0 -1
  3. package/dist/adapters/databasePluginUtils.cjs +0 -1
  4. package/dist/adapters/drizzle.cjs +11 -4
  5. package/dist/adapters/drizzle.d.cts +0 -1
  6. package/dist/adapters/drizzle.d.mts +0 -1
  7. package/dist/adapters/drizzle.mjs +12 -4
  8. package/dist/adapters/drizzleCrud.cjs +62 -1
  9. package/dist/adapters/drizzleCrud.mjs +61 -2
  10. package/dist/adapters/drizzleLazyDB.cjs +6 -1
  11. package/dist/adapters/drizzleLazyDB.mjs +6 -1
  12. package/dist/adapters/drizzleQuery.cjs +3 -2
  13. package/dist/adapters/drizzleQuery.mjs +3 -1
  14. package/dist/adapters/insightsTransaction.cjs +22 -0
  15. package/dist/adapters/insightsTransaction.mjs +22 -0
  16. package/dist/adapters/kysely.cjs +2 -1
  17. package/dist/adapters/kysely.d.cts +0 -1
  18. package/dist/adapters/kysely.d.mts +0 -1
  19. package/dist/adapters/kysely.mjs +3 -1
  20. package/dist/adapters/kyselyCrud.cjs +54 -3
  21. package/dist/adapters/kyselyCrud.mjs +54 -3
  22. package/dist/adapters/kyselyQuery.cjs +2 -2
  23. package/dist/adapters/kyselyQuery.mjs +2 -1
  24. package/dist/adapters/mongodb.cjs +27 -1
  25. package/dist/adapters/mongodb.d.cts +1 -1
  26. package/dist/adapters/mongodb.d.mts +1 -1
  27. package/dist/adapters/mongodb.mjs +28 -1
  28. package/dist/adapters/mongodbCollections.cjs +2 -0
  29. package/dist/adapters/mongodbCollections.mjs +2 -0
  30. package/dist/adapters/mongodbQuery.cjs +21 -4
  31. package/dist/adapters/mongodbQuery.mjs +21 -5
  32. package/dist/adapters/mongodbReads.cjs +91 -29
  33. package/dist/adapters/mongodbReads.mjs +92 -29
  34. package/dist/adapters/mongodbWrites.cjs +16 -2
  35. package/dist/adapters/mongodbWrites.mjs +18 -3
  36. package/dist/adapters/prisma.cjs +55 -7
  37. package/dist/adapters/prisma.d.cts +0 -1
  38. package/dist/adapters/prisma.d.mts +0 -1
  39. package/dist/adapters/prisma.mjs +56 -7
  40. package/dist/adapters/prismaQuery.cjs +0 -1
  41. package/dist/adapters/prismaRows.cjs +22 -1
  42. package/dist/adapters/prismaRows.mjs +22 -1
  43. package/dist/apiKeys.cjs +6 -2
  44. package/dist/apiKeys.d.cts +0 -1
  45. package/dist/apiKeys.d.mts +0 -1
  46. package/dist/apiKeys.mjs +3 -3
  47. package/dist/createHotUpdaterCore.cjs +27 -12
  48. package/dist/createHotUpdaterCore.d.cts +8 -8
  49. package/dist/createHotUpdaterCore.d.mts +8 -8
  50. package/dist/createHotUpdaterCore.mjs +26 -12
  51. package/dist/db/createBundleDiff.cjs +2 -2
  52. package/dist/db/createBundleDiff.d.cts +1 -5
  53. package/dist/db/createBundleDiff.d.mts +1 -5
  54. package/dist/db/databasePluginCore.cjs +0 -1
  55. package/dist/db/fixedMigrator.cjs +2 -2
  56. package/dist/db/fixedMigrator.mjs +2 -2
  57. package/dist/db/fixedMigratorMongo.cjs +6 -4
  58. package/dist/db/fixedMigratorMongo.mjs +6 -4
  59. package/dist/db/index.d.cts +0 -1
  60. package/dist/db/index.d.mts +0 -1
  61. package/dist/db/releaseCatalog.cjs +0 -1
  62. package/dist/db/releaseCatalog.d.cts +0 -1
  63. package/dist/db/releaseCatalog.d.mts +0 -1
  64. package/dist/db/schema/insightsCollation.cjs +14 -0
  65. package/dist/db/schema/insightsCollation.mjs +14 -0
  66. package/dist/db/schema/sql.cjs +11 -7
  67. package/dist/db/schema/sql.mjs +8 -8
  68. package/dist/db/schemaGenerators.cjs +28 -9
  69. package/dist/db/schemaGenerators.mjs +28 -9
  70. package/dist/db/schemaReadiness.cjs +2 -0
  71. package/dist/db/schemaReadiness.d.cts +1 -0
  72. package/dist/db/schemaReadiness.d.mts +1 -0
  73. package/dist/db/schemaReadiness.mjs +2 -0
  74. package/dist/db/sqlMigrationExecution.cjs +1 -1
  75. package/dist/db/sqlMigrationExecution.mjs +1 -0
  76. package/dist/db/types.cjs +3 -1
  77. package/dist/db/types.d.cts +3 -6
  78. package/dist/db/types.d.mts +3 -6
  79. package/dist/db/types.mjs +2 -2
  80. package/dist/db/updateArtifacts.cjs +0 -1
  81. package/dist/handler.cjs +5 -5
  82. package/dist/handler.d.cts +1 -2
  83. package/dist/handler.d.mts +1 -2
  84. package/dist/handler.mjs +5 -5
  85. package/dist/handlerReleaseCatalogRoutes.cjs +0 -1
  86. package/dist/handlerReleaseManagementRoutes.cjs +0 -1
  87. package/dist/handlerTypes.d.cts +0 -1
  88. package/dist/handlerTypes.d.mts +0 -1
  89. package/dist/handlerVersionRoutes.d.cts +1 -0
  90. package/dist/handlerVersionRoutes.d.mts +1 -0
  91. package/dist/index.cjs +2 -2
  92. package/dist/index.d.cts +4 -4
  93. package/dist/index.d.mts +4 -4
  94. package/dist/index.mjs +2 -2
  95. package/dist/insights/domain.d.cts +89 -0
  96. package/dist/insights/domain.d.mts +89 -0
  97. package/dist/insights/errors.cjs +15 -0
  98. package/dist/insights/errors.mjs +14 -0
  99. package/dist/{analytics → insights}/eventInput.cjs +58 -14
  100. package/dist/{analytics → insights}/eventInput.mjs +58 -16
  101. package/dist/insights/provider.cjs +314 -0
  102. package/dist/insights/provider.d.cts +6 -0
  103. package/dist/insights/provider.d.mts +6 -0
  104. package/dist/insights/provider.mjs +314 -0
  105. package/dist/insights/queryInput.cjs +93 -0
  106. package/dist/insights/queryInput.mjs +91 -0
  107. package/dist/insights/routes.cjs +58 -0
  108. package/dist/insights/routes.mjs +56 -0
  109. package/dist/insights/types.d.cts +32 -0
  110. package/dist/insights/types.d.mts +32 -0
  111. package/dist/node.d.cts +0 -1
  112. package/dist/node.d.mts +0 -1
  113. package/dist/package.cjs +1 -1
  114. package/dist/package.mjs +1 -1
  115. package/dist/schema/index.cjs +17 -3
  116. package/dist/schema/index.mjs +4 -4
  117. package/dist/schema/v1_0_0.cjs +72 -30
  118. package/dist/schema/v1_0_0.mjs +72 -31
  119. package/dist/storageAccess.cjs +0 -1
  120. package/dist/types/index.d.cts +0 -1
  121. package/dist/types/index.d.mts +0 -1
  122. package/package.json +7 -7
  123. package/src/adapters/databasePluginTestDatabase.ts +14 -0
  124. package/src/adapters/drizzle.spec.ts +99 -0
  125. package/src/adapters/drizzle.ts +22 -4
  126. package/src/adapters/drizzleCrud.ts +154 -2
  127. package/src/adapters/drizzleLazyDB.ts +17 -3
  128. package/src/adapters/insightsTransaction.ts +32 -0
  129. package/src/adapters/kysely.spec.ts +29 -0
  130. package/src/adapters/kysely.ts +10 -1
  131. package/src/adapters/kyselyCrud.ts +121 -0
  132. package/src/adapters/mongodb.integration.spec.ts +210 -0
  133. package/src/adapters/mongodb.spec.ts +69 -1
  134. package/src/adapters/mongodb.ts +66 -2
  135. package/src/adapters/mongodbCollections.ts +5 -0
  136. package/src/adapters/mongodbQuery.ts +32 -3
  137. package/src/adapters/mongodbReads.ts +69 -0
  138. package/src/adapters/mongodbTestClient.ts +41 -6
  139. package/src/adapters/mongodbTestFilter.ts +4 -1
  140. package/src/adapters/mongodbWrites.ts +27 -0
  141. package/src/adapters/prisma.spec.ts +97 -0
  142. package/src/adapters/prisma.ts +103 -3
  143. package/src/adapters/prismaRows.ts +37 -1
  144. package/src/adapters/prismaTestClient.ts +22 -5
  145. package/src/apiKeys.integration.spec.ts +3 -3
  146. package/src/createHotUpdaterCore.ts +30 -16
  147. package/src/db/databasePluginUpdateCheck.bench.ts +3 -0
  148. package/src/db/fixedMigrator.spec.ts +65 -0
  149. package/src/db/fixedMigratorMongo.spec.ts +56 -9
  150. package/src/db/fixedMigratorMongo.ts +10 -10
  151. package/src/db/index.spec.ts +8 -1
  152. package/src/db/schema/insightsCollation.ts +21 -0
  153. package/src/db/schema/sql.ts +8 -5
  154. package/src/db/schemaGenerators.ts +41 -13
  155. package/src/db/types.spec.ts +11 -3
  156. package/src/db/types.ts +13 -7
  157. package/src/handler-standalone.integration.spec.ts +6 -8
  158. package/src/handler.ts +12 -12
  159. package/src/index.ts +3 -3
  160. package/src/insights/domain.ts +103 -0
  161. package/src/insights/errors.ts +11 -0
  162. package/src/{analytics → insights}/eventInput.ts +71 -14
  163. package/src/insights/insights.integration.spec.ts +381 -0
  164. package/src/insights/provider.spec.ts +385 -0
  165. package/src/insights/provider.ts +591 -0
  166. package/src/insights/queryInput.ts +141 -0
  167. package/src/insights/routes.ts +104 -0
  168. package/src/insights/types.ts +55 -0
  169. package/src/schema/dsl-all-versions.spec.ts +3 -1
  170. package/src/schema/v1_0_0.spec.ts +77 -3
  171. package/src/schema/v1_0_0.ts +94 -30
  172. package/dist/analytics/bounded/activeOverview.cjs +0 -87
  173. package/dist/analytics/bounded/activeOverview.mjs +0 -85
  174. package/dist/analytics/bounded/installationSearch.cjs +0 -42
  175. package/dist/analytics/bounded/installationSearch.mjs +0 -42
  176. package/dist/analytics/bounded/persistence.cjs +0 -39
  177. package/dist/analytics/bounded/persistence.mjs +0 -38
  178. package/dist/analytics/bounded/provider.cjs +0 -170
  179. package/dist/analytics/bounded/provider.d.cts +0 -7
  180. package/dist/analytics/bounded/provider.d.mts +0 -7
  181. package/dist/analytics/bounded/provider.mjs +0 -170
  182. package/dist/analytics/bounded/scan.cjs +0 -144
  183. package/dist/analytics/bounded/scan.mjs +0 -138
  184. package/dist/analytics/domain.d.cts +0 -121
  185. package/dist/analytics/domain.d.mts +0 -121
  186. package/dist/analytics/errors.cjs +0 -22
  187. package/dist/analytics/errors.mjs +0 -20
  188. package/dist/analytics/persistence.d.cts +0 -6
  189. package/dist/analytics/persistence.d.mts +0 -6
  190. package/dist/analytics/queryInput.cjs +0 -58
  191. package/dist/analytics/queryInput.mjs +0 -55
  192. package/dist/analytics/routes.cjs +0 -63
  193. package/dist/analytics/routes.mjs +0 -61
  194. package/dist/analytics/types.d.cts +0 -17
  195. package/dist/analytics/types.d.mts +0 -17
  196. package/src/analytics/analytics.integration.spec.ts +0 -144
  197. package/src/analytics/bounded/activeOverview.spec.ts +0 -189
  198. package/src/analytics/bounded/activeOverview.ts +0 -148
  199. package/src/analytics/bounded/installationSearch.spec.ts +0 -130
  200. package/src/analytics/bounded/installationSearch.ts +0 -69
  201. package/src/analytics/bounded/persistence.ts +0 -43
  202. package/src/analytics/bounded/provider.spec.ts +0 -219
  203. package/src/analytics/bounded/provider.ts +0 -229
  204. package/src/analytics/bounded/scan.ts +0 -235
  205. package/src/analytics/domain.ts +0 -139
  206. package/src/analytics/errors.ts +0 -18
  207. package/src/analytics/persistence.ts +0 -10
  208. package/src/analytics/queryInput.ts +0 -102
  209. package/src/analytics/routes.ts +0 -123
  210. package/src/analytics/types.ts +0 -44
@@ -1,55 +0,0 @@
1
- import { AnalyticsBadRequestError } from "./errors.mjs";
2
- //#region src/analytics/queryInput.ts
3
- const EVENT_LIST_BOUNDS = {
4
- defaultValue: 50,
5
- maximum: 100,
6
- minimum: 1
7
- };
8
- const EVENT_LIST_OFFSET_BOUNDS = {
9
- defaultValue: 0,
10
- minimum: 0
11
- };
12
- const MAX_USER_ID_LENGTH = 1024;
13
- function parseInteger(url, key, bounds) {
14
- const value = url.searchParams.get(key);
15
- if (value === null) return bounds.defaultValue;
16
- const parsed = Number(value);
17
- if (!Number.isSafeInteger(parsed) || parsed < bounds.minimum || bounds.maximum !== void 0 && parsed > bounds.maximum) throw new AnalyticsBadRequestError(`Invalid '${key}' query parameter.`);
18
- return parsed;
19
- }
20
- const parsePagination = (request) => {
21
- const url = new URL(request.url);
22
- return {
23
- limit: parseInteger(url, "limit", EVENT_LIST_BOUNDS),
24
- offset: parseInteger(url, "offset", EVENT_LIST_OFFSET_BOUNDS)
25
- };
26
- };
27
- const parseAnalyticsQuery = (request) => {
28
- const window = new URL(request.url).searchParams.get("window") ?? "24h";
29
- if (window !== "24h" && window !== "7d" && window !== "30d" && window !== "all") throw new AnalyticsBadRequestError("Invalid 'window' query parameter.");
30
- return {
31
- ...parsePagination(request),
32
- window
33
- };
34
- };
35
- const parseActiveInstallationInput = (request) => {
36
- const url = new URL(request.url);
37
- const windows = url.searchParams.getAll("window");
38
- if (windows.length > 1) throw new AnalyticsBadRequestError("Duplicate 'window' query parameter.");
39
- const window = windows[0] ?? "30d";
40
- if (window !== "24h" && window !== "7d" && window !== "30d") throw new AnalyticsBadRequestError("Invalid 'window' query parameter.");
41
- const userIds = url.searchParams.getAll("userId");
42
- if (userIds.length > 1) throw new AnalyticsBadRequestError("Duplicate 'userId' query parameter.");
43
- const userId = userIds[0];
44
- if (userId !== void 0 && (userId.length === 0 || userId.length > MAX_USER_ID_LENGTH)) throw new AnalyticsBadRequestError("Invalid 'userId' query parameter.");
45
- return userId === void 0 ? { window } : {
46
- window,
47
- userId
48
- };
49
- };
50
- const parseSearchInput = (request) => ({
51
- ...parsePagination(request),
52
- query: new URL(request.url).searchParams.get("query")?.trim() ?? ""
53
- });
54
- //#endregion
55
- export { parseActiveInstallationInput, parseAnalyticsQuery, parsePagination, parseSearchInput };
@@ -1,63 +0,0 @@
1
- const require_errors = require("./errors.cjs");
2
- const require_eventInput = require("./eventInput.cjs");
3
- const require_queryInput = require("./queryInput.cjs");
4
- //#region src/analytics/routes.ts
5
- const json = (body, status) => Response.json(body, {
6
- headers: { "cache-control": "private, no-store" },
7
- status
8
- });
9
- const requireParam = (params, key) => {
10
- const value = params[key];
11
- if (value === void 0 || value.length === 0) throw new require_errors.AnalyticsBadRequestError(`Missing route parameter: ${key}`);
12
- return value;
13
- };
14
- const run = async (operation) => {
15
- try {
16
- return await operation();
17
- } catch (error) {
18
- if (error instanceof require_errors.AnalyticsBadRequestError) return json({ error: error.message }, 400);
19
- if (error instanceof require_errors.AnalyticsPayloadTooLargeError) return json({ error: error.message }, 413);
20
- if (error instanceof require_errors.AnalyticsScanLimitExceededError) return json({ error: {
21
- code: "ANALYTICS_SCAN_LIMIT_EXCEEDED",
22
- limit: error.limit
23
- } }, 503);
24
- throw error;
25
- }
26
- };
27
- const query = (operation) => run(async () => json(await operation(), 200));
28
- const createAnalyticsRouteHandlers = (provider) => ({
29
- appendBundleEvent: async (_params, request) => run(async () => {
30
- await provider.appendBundleEvent(await require_eventInput.parseBundleEventRequest(request));
31
- return new Response(null, { status: 204 });
32
- }),
33
- getBundleEventSummary: (params) => query(() => provider.getBundleEventSummary(requireParam(params, "id"))),
34
- getBundleEventAnalytics: (params, request) => query(() => {
35
- const input = require_queryInput.parseAnalyticsQuery(request);
36
- return provider.getBundleEventAnalytics(requireParam(params, "id"), input.window, input.limit, input.offset);
37
- }),
38
- getBundleEventOverview: () => query(() => provider.getBundleEventOverview()),
39
- getActiveInstallationOverview: (_params, request) => query(() => provider.getActiveInstallationOverview(require_queryInput.parseActiveInstallationInput(request))),
40
- searchInstallations: (_params, request) => query(() => {
41
- const input = require_queryInput.parseSearchInput(request);
42
- return provider.searchInstallations(input.query, input.limit, input.offset);
43
- }),
44
- getInstallationHistory: (params, request) => query(() => {
45
- const input = require_queryInput.parsePagination(request);
46
- return provider.getInstallationHistory(requireParam(params, "installId"), input.limit, input.offset);
47
- })
48
- });
49
- const registerAnalyticsClientRoutes = (add) => {
50
- add("POST", "/events", "appendBundleEvent");
51
- };
52
- const registerAnalyticsAdminRoutes = (add) => {
53
- add("GET", "/bundles/:id/events/summary", "getBundleEventSummary");
54
- add("GET", "/bundles/:id/events/analytics", "getBundleEventAnalytics");
55
- add("GET", "/installations/overview", "getBundleEventOverview");
56
- add("GET", "/installations/active", "getActiveInstallationOverview");
57
- add("GET", "/installations", "searchInstallations");
58
- add("GET", "/installations/:installId/events", "getInstallationHistory");
59
- };
60
- //#endregion
61
- exports.createAnalyticsRouteHandlers = createAnalyticsRouteHandlers;
62
- exports.registerAnalyticsAdminRoutes = registerAnalyticsAdminRoutes;
63
- exports.registerAnalyticsClientRoutes = registerAnalyticsClientRoutes;
@@ -1,61 +0,0 @@
1
- import { AnalyticsBadRequestError, AnalyticsPayloadTooLargeError, AnalyticsScanLimitExceededError } from "./errors.mjs";
2
- import { parseBundleEventRequest } from "./eventInput.mjs";
3
- import { parseActiveInstallationInput, parseAnalyticsQuery, parsePagination, parseSearchInput } from "./queryInput.mjs";
4
- //#region src/analytics/routes.ts
5
- const json = (body, status) => Response.json(body, {
6
- headers: { "cache-control": "private, no-store" },
7
- status
8
- });
9
- const requireParam = (params, key) => {
10
- const value = params[key];
11
- if (value === void 0 || value.length === 0) throw new AnalyticsBadRequestError(`Missing route parameter: ${key}`);
12
- return value;
13
- };
14
- const run = async (operation) => {
15
- try {
16
- return await operation();
17
- } catch (error) {
18
- if (error instanceof AnalyticsBadRequestError) return json({ error: error.message }, 400);
19
- if (error instanceof AnalyticsPayloadTooLargeError) return json({ error: error.message }, 413);
20
- if (error instanceof AnalyticsScanLimitExceededError) return json({ error: {
21
- code: "ANALYTICS_SCAN_LIMIT_EXCEEDED",
22
- limit: error.limit
23
- } }, 503);
24
- throw error;
25
- }
26
- };
27
- const query = (operation) => run(async () => json(await operation(), 200));
28
- const createAnalyticsRouteHandlers = (provider) => ({
29
- appendBundleEvent: async (_params, request) => run(async () => {
30
- await provider.appendBundleEvent(await parseBundleEventRequest(request));
31
- return new Response(null, { status: 204 });
32
- }),
33
- getBundleEventSummary: (params) => query(() => provider.getBundleEventSummary(requireParam(params, "id"))),
34
- getBundleEventAnalytics: (params, request) => query(() => {
35
- const input = parseAnalyticsQuery(request);
36
- return provider.getBundleEventAnalytics(requireParam(params, "id"), input.window, input.limit, input.offset);
37
- }),
38
- getBundleEventOverview: () => query(() => provider.getBundleEventOverview()),
39
- getActiveInstallationOverview: (_params, request) => query(() => provider.getActiveInstallationOverview(parseActiveInstallationInput(request))),
40
- searchInstallations: (_params, request) => query(() => {
41
- const input = parseSearchInput(request);
42
- return provider.searchInstallations(input.query, input.limit, input.offset);
43
- }),
44
- getInstallationHistory: (params, request) => query(() => {
45
- const input = parsePagination(request);
46
- return provider.getInstallationHistory(requireParam(params, "installId"), input.limit, input.offset);
47
- })
48
- });
49
- const registerAnalyticsClientRoutes = (add) => {
50
- add("POST", "/events", "appendBundleEvent");
51
- };
52
- const registerAnalyticsAdminRoutes = (add) => {
53
- add("GET", "/bundles/:id/events/summary", "getBundleEventSummary");
54
- add("GET", "/bundles/:id/events/analytics", "getBundleEventAnalytics");
55
- add("GET", "/installations/overview", "getBundleEventOverview");
56
- add("GET", "/installations/active", "getActiveInstallationOverview");
57
- add("GET", "/installations", "searchInstallations");
58
- add("GET", "/installations/:installId/events", "getInstallationHistory");
59
- };
60
- //#endregion
61
- export { createAnalyticsRouteHandlers, registerAnalyticsAdminRoutes, registerAnalyticsClientRoutes };
@@ -1,17 +0,0 @@
1
- import { ActiveInstallationInput, ActiveInstallationOverview, BundleEventAnalyticsResult, BundleEventAnalyticsWindow, BundleEventOverview, BundleEventSummary, BundleEventSummaryByBundle, CreateBundleEventRequest, InstallationHistoryRow, InstallationSearchRow, OffsetPaginationResult } from "./domain.cjs";
2
-
3
- //#region src/analytics/types.d.ts
4
- type AnalyticsProvider = {
5
- readonly mode: "bounded";
6
- readonly maxMatchingRows: number;
7
- appendBundleEvent(input: CreateBundleEventRequest): Promise<void>;
8
- getBundleEventSummary(bundleId: string): Promise<BundleEventSummary>;
9
- getBundleEventSummaries(bundleIds: readonly string[], window: BundleEventAnalyticsWindow): Promise<readonly BundleEventSummaryByBundle[]>;
10
- getBundleEventAnalytics(bundleId: string, window: BundleEventAnalyticsWindow, limit: number, offset: number): Promise<BundleEventAnalyticsResult>;
11
- getBundleEventOverview(): Promise<BundleEventOverview>;
12
- getActiveInstallationOverview(input: ActiveInstallationInput): Promise<ActiveInstallationOverview>;
13
- searchInstallations(query: string, limit: number, offset: number): Promise<OffsetPaginationResult<InstallationSearchRow>>;
14
- getInstallationHistory(installId: string, limit: number, offset: number): Promise<OffsetPaginationResult<InstallationHistoryRow>>;
15
- };
16
- //#endregion
17
- export { AnalyticsProvider };
@@ -1,17 +0,0 @@
1
- import { ActiveInstallationInput, ActiveInstallationOverview, BundleEventAnalyticsResult, BundleEventAnalyticsWindow, BundleEventOverview, BundleEventSummary, BundleEventSummaryByBundle, CreateBundleEventRequest, InstallationHistoryRow, InstallationSearchRow, OffsetPaginationResult } from "./domain.mjs";
2
-
3
- //#region src/analytics/types.d.ts
4
- type AnalyticsProvider = {
5
- readonly mode: "bounded";
6
- readonly maxMatchingRows: number;
7
- appendBundleEvent(input: CreateBundleEventRequest): Promise<void>;
8
- getBundleEventSummary(bundleId: string): Promise<BundleEventSummary>;
9
- getBundleEventSummaries(bundleIds: readonly string[], window: BundleEventAnalyticsWindow): Promise<readonly BundleEventSummaryByBundle[]>;
10
- getBundleEventAnalytics(bundleId: string, window: BundleEventAnalyticsWindow, limit: number, offset: number): Promise<BundleEventAnalyticsResult>;
11
- getBundleEventOverview(): Promise<BundleEventOverview>;
12
- getActiveInstallationOverview(input: ActiveInstallationInput): Promise<ActiveInstallationOverview>;
13
- searchInstallations(query: string, limit: number, offset: number): Promise<OffsetPaginationResult<InstallationSearchRow>>;
14
- getInstallationHistory(installId: string, limit: number, offset: number): Promise<OffsetPaginationResult<InstallationHistoryRow>>;
15
- };
16
- //#endregion
17
- export { AnalyticsProvider };
@@ -1,144 +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
- sdkVersion: "2.0.0",
22
- } as const;
23
-
24
- const eventRequest = (body: unknown = event) =>
25
- new Request("https://example.com/events", {
26
- method: "POST",
27
- headers: { "content-type": "application/json" },
28
- body: JSON.stringify(body),
29
- });
30
-
31
- afterEach(() => {
32
- vi.useRealTimers();
33
- });
34
-
35
- describe("createHotUpdater Analytics", () => {
36
- it("always persists events and serves queries without a server Analytics flag", async () => {
37
- vi.useFakeTimers();
38
- vi.setSystemTime(new Date("2026-08-12T00:00:00.000Z"));
39
- const database = createInMemoryDatabasePlugin();
40
- const append = vi.spyOn(database.models.analytics, "append");
41
- const hotUpdater = createHotUpdater({
42
- database,
43
- clientAccess: { type: "public" },
44
- });
45
-
46
- const ingestion = await hotUpdater.handlers.client(eventRequest());
47
- vi.advanceTimersByTime(1);
48
- const overview = await hotUpdater.handlers.admin(
49
- new Request("https://example.com/installations/overview"),
50
- );
51
-
52
- expect(ingestion.status).toBe(204);
53
- expect(append).toHaveBeenCalledWith(
54
- expect.objectContaining({
55
- install_id: "install-1",
56
- sdk_version: "2.0.0",
57
- to_bundle_id: "bundle-1",
58
- type: "UNCHANGED",
59
- }),
60
- );
61
- expect(hotUpdater.analytics).toMatchObject({
62
- mode: "bounded",
63
- maxMatchingRows: 50_000,
64
- });
65
- expect(overview.status).toBe(200);
66
- await expect(overview.json()).resolves.toEqual({
67
- bundles: [{ bundleId: "bundle-1", installations: 1 }],
68
- trackedInstallations: 1,
69
- });
70
- });
71
-
72
- it("checks the official schema before Analytics reads and writes", async () => {
73
- const database = createSchemaManagedDatabase(
74
- "kysely",
75
- HOT_UPDATER_SCHEMA_VERSION,
76
- );
77
- const createMigrator = vi.spyOn(database, "createMigrator");
78
- const hotUpdater = createHotUpdater({
79
- database,
80
- clientAccess: { type: "public" },
81
- });
82
-
83
- expect((await hotUpdater.handlers.client(eventRequest())).status).toBe(204);
84
- await hotUpdater.analytics?.getBundleEventOverview();
85
-
86
- expect(createMigrator).toHaveBeenCalledOnce();
87
- });
88
-
89
- it("keeps ingestion and queries on separate handler surfaces", async () => {
90
- const hotUpdater = createHotUpdater({
91
- database: createInMemoryDatabasePlugin(),
92
- clientAccess: { type: "public" },
93
- });
94
-
95
- expect((await hotUpdater.handlers.client(eventRequest())).status).toBe(204);
96
- const clientQuery = await hotUpdater.handlers.client(
97
- new Request("https://example.com/installations/overview"),
98
- );
99
- const adminIngestion = await hotUpdater.handlers.admin(eventRequest());
100
- const adminQuery = await hotUpdater.handlers.admin(
101
- new Request("https://example.com/installations/overview"),
102
- );
103
-
104
- expect(clientQuery.status).toBe(404);
105
- expect(adminIngestion.status).toBe(404);
106
- expect(adminQuery.status).toBe(200);
107
- expect(adminQuery.headers.get("cache-control")).toBe("private, no-store");
108
- });
109
-
110
- it("returns a stable client error for malformed event payloads", async () => {
111
- const hotUpdater = createHotUpdater({
112
- database: createInMemoryDatabasePlugin(),
113
- clientAccess: { type: "public" },
114
- });
115
-
116
- const response = await hotUpdater.handlers.client(
117
- eventRequest({ ...event, platform: "web" }),
118
- );
119
-
120
- expect(response.status).toBe(400);
121
- await expect(response.json()).resolves.toEqual({
122
- error: "Invalid event field: platform",
123
- });
124
- });
125
-
126
- it.each(["fromReleaseId", "toReleaseId"] as const)(
127
- "rejects an omitted %s instead of treating it as null",
128
- async (field) => {
129
- const hotUpdater = createHotUpdater({
130
- database: createInMemoryDatabasePlugin(),
131
- clientAccess: { type: "public" },
132
- });
133
- const payload: Record<string, unknown> = { ...event };
134
- delete payload[field];
135
-
136
- const response = await hotUpdater.handlers.client(eventRequest(payload));
137
-
138
- expect(response.status).toBe(400);
139
- await expect(response.json()).resolves.toEqual({
140
- error: `Invalid event field: ${field}`,
141
- });
142
- },
143
- );
144
- });
@@ -1,189 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- import type { BundleEventPersistenceRow } from "../persistence";
4
- import { collectActiveInstallationOverview } from "./activeOverview";
5
-
6
- const HOUR_MS = 60 * 60 * 1_000;
7
- const DAY_MS = 24 * HOUR_MS;
8
-
9
- type EventRowOptions = {
10
- readonly id: string;
11
- readonly receivedAtMs: number;
12
- readonly installId?: string;
13
- readonly userId?: string | null;
14
- readonly bundleId?: string;
15
- };
16
-
17
- type SeriesWindow = {
18
- readonly startMs: number;
19
- readonly bucketCount: number;
20
- readonly bucketSizeMs: number;
21
- };
22
-
23
- function eventRow(options: EventRowOptions): BundleEventPersistenceRow {
24
- return {
25
- id: options.id,
26
- type: "UPDATE_APPLIED",
27
- install_id: options.installId ?? `install-${options.id}`,
28
- user_id: options.userId ?? null,
29
- username: null,
30
- from_release_id: null,
31
- from_bundle_id: "old",
32
- to_release_id: null,
33
- to_bundle_id: options.bundleId ?? "new",
34
- platform: "ios",
35
- app_version: "1.0.0",
36
- channel: "production",
37
- cohort: "default",
38
- update_strategy: "fingerprint",
39
- fingerprint_hash: null,
40
- sdk_version: null,
41
- received_at_ms: options.receivedAtMs,
42
- };
43
- }
44
-
45
- function expectedSeries(
46
- window: SeriesWindow,
47
- values: Readonly<Record<number, number>>,
48
- ): { readonly bucketStartMs: number; readonly value: number }[] {
49
- return Array.from({ length: window.bucketCount }, (_, index) => ({
50
- bucketStartMs: window.startMs + index * window.bucketSizeMs,
51
- value: values[index] ?? 0,
52
- }));
53
- }
54
-
55
- describe("collectActiveInstallationOverview", () => {
56
- it("uses the strict window and latest installation rows for a user", () => {
57
- const asOfMs = 100 * HOUR_MS;
58
- const windowStartMs = asOfMs - 24 * HOUR_MS;
59
- const window = {
60
- startMs: windowStartMs,
61
- bucketCount: 24,
62
- bucketSizeMs: HOUR_MS,
63
- } as const;
64
- const rows = [
65
- eventRow({
66
- id: "a-start",
67
- receivedAtMs: windowStartMs,
68
- installId: "install-a",
69
- userId: "user-a",
70
- bundleId: "bundle-b",
71
- }),
72
- eventRow({
73
- id: "a-latest",
74
- receivedAtMs: asOfMs - 1,
75
- installId: "install-a",
76
- userId: "user-a",
77
- bundleId: "bundle-a",
78
- }),
79
- eventRow({
80
- id: "before-window",
81
- receivedAtMs: windowStartMs - 1,
82
- userId: "user-a",
83
- }),
84
- eventRow({
85
- id: "at-cutoff",
86
- receivedAtMs: asOfMs,
87
- userId: "user-a",
88
- }),
89
- eventRow({
90
- id: "tie-a",
91
- receivedAtMs: asOfMs - HOUR_MS,
92
- installId: "install-tie",
93
- userId: "user-a",
94
- bundleId: "bundle-b",
95
- }),
96
- eventRow({
97
- id: "tie-z",
98
- receivedAtMs: asOfMs - HOUR_MS,
99
- installId: "install-tie",
100
- userId: "user-a",
101
- bundleId: "bundle-c",
102
- }),
103
- eventRow({
104
- id: "changed-user-old",
105
- receivedAtMs: windowStartMs,
106
- installId: "install-changed-user",
107
- userId: "user-a",
108
- }),
109
- eventRow({
110
- id: "changed-user-new",
111
- receivedAtMs: asOfMs - 1,
112
- installId: "install-changed-user",
113
- userId: "user-b",
114
- }),
115
- ];
116
-
117
- expect(
118
- collectActiveInstallationOverview({
119
- rows,
120
- asOfMs,
121
- window: "24h",
122
- userId: "user-a",
123
- }),
124
- ).toEqual({
125
- asOfMs,
126
- window: "24h",
127
- activeInstallations: 2,
128
- series: expectedSeries(window, { 0: 1, 23: 2 }),
129
- bundleSeries: [
130
- {
131
- bundleId: "bundle-a",
132
- series: expectedSeries(window, { 23: 1 }),
133
- },
134
- {
135
- bundleId: "bundle-b",
136
- series: expectedSeries(window, { 0: 1 }),
137
- },
138
- {
139
- bundleId: "bundle-c",
140
- series: expectedSeries(window, { 23: 1 }),
141
- },
142
- ],
143
- bundles: [
144
- { bundleId: "bundle-a", installations: 1 },
145
- { bundleId: "bundle-c", installations: 1 },
146
- ],
147
- });
148
- });
149
-
150
- it("includes installations with any user when the filter is omitted", () => {
151
- const asOfMs = 10 * DAY_MS;
152
- const windowStartMs = asOfMs - 7 * DAY_MS;
153
- const window = {
154
- startMs: windowStartMs,
155
- bucketCount: 7,
156
- bucketSizeMs: DAY_MS,
157
- } as const;
158
- const rows = [
159
- eventRow({
160
- id: "user-a",
161
- receivedAtMs: windowStartMs,
162
- userId: "user-a",
163
- bundleId: "bundle-b",
164
- }),
165
- eventRow({
166
- id: "anonymous",
167
- receivedAtMs: asOfMs - 1,
168
- bundleId: "bundle-a",
169
- }),
170
- ];
171
-
172
- const result = collectActiveInstallationOverview({
173
- rows,
174
- asOfMs,
175
- window: "7d",
176
- });
177
-
178
- expect(result.activeInstallations).toBe(2);
179
- expect(result.series).toEqual(expectedSeries(window, { 0: 1, 6: 1 }));
180
- expect(result.bundles).toEqual([
181
- { bundleId: "bundle-a", installations: 1 },
182
- { bundleId: "bundle-b", installations: 1 },
183
- ]);
184
- expect(result.bundleSeries.map(({ bundleId }) => bundleId)).toEqual([
185
- "bundle-a",
186
- "bundle-b",
187
- ]);
188
- });
189
- });