@hot-updater/server 1.0.0-rc.1 → 1.0.0-rc.16

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 (542) hide show
  1. package/dist/_virtual/_rolldown/runtime.cjs +1 -1
  2. package/dist/adapters/drizzle.cjs +35 -69
  3. package/dist/adapters/drizzle.d.cts +13 -6
  4. package/dist/adapters/drizzle.d.mts +13 -6
  5. package/dist/adapters/drizzle.mjs +34 -69
  6. package/dist/adapters/drizzleExecutor.cjs +114 -0
  7. package/dist/adapters/drizzleExecutor.d.cts +9 -0
  8. package/dist/adapters/drizzleExecutor.d.mts +9 -0
  9. package/dist/adapters/drizzleExecutor.mjs +112 -0
  10. package/dist/adapters/kysely.cjs +25 -31
  11. package/dist/adapters/kysely.d.cts +10 -7
  12. package/dist/adapters/kysely.d.mts +10 -7
  13. package/dist/adapters/kysely.mjs +25 -31
  14. package/dist/adapters/kyselyExecutor.cjs +32 -0
  15. package/dist/adapters/kyselyExecutor.d.cts +12 -0
  16. package/dist/adapters/kyselyExecutor.d.mts +12 -0
  17. package/dist/adapters/kyselyExecutor.mjs +32 -0
  18. package/dist/adapters/mongodb.cjs +28 -27
  19. package/dist/adapters/mongodb.d.cts +10 -5
  20. package/dist/adapters/mongodb.d.mts +10 -5
  21. package/dist/adapters/mongodb.mjs +28 -27
  22. package/dist/adapters/mongodbAdapter.cjs +174 -0
  23. package/dist/adapters/mongodbAdapter.d.cts +9 -0
  24. package/dist/adapters/mongodbAdapter.d.mts +9 -0
  25. package/dist/adapters/mongodbAdapter.mjs +173 -0
  26. package/dist/adapters/prisma.cjs +38 -220
  27. package/dist/adapters/prisma.d.cts +12 -8
  28. package/dist/adapters/prisma.d.mts +12 -8
  29. package/dist/adapters/prisma.mjs +38 -219
  30. package/dist/adapters/prismaExecutor.cjs +64 -0
  31. package/dist/adapters/prismaExecutor.mjs +64 -0
  32. package/dist/adapters/sqlProviders.cjs +9 -0
  33. package/dist/adapters/sqlProviders.mjs +9 -0
  34. package/dist/apiKeys.cjs +8 -8
  35. package/dist/apiKeys.d.cts +1 -24
  36. package/dist/apiKeys.d.mts +1 -24
  37. package/dist/apiKeys.mjs +5 -9
  38. package/dist/assembly/assemblePlugins.cjs +147 -0
  39. package/dist/assembly/assemblePlugins.d.cts +12 -0
  40. package/dist/assembly/assemblePlugins.d.mts +12 -0
  41. package/dist/assembly/assemblePlugins.mjs +146 -0
  42. package/dist/assembly/databasePlugins.cjs +23 -0
  43. package/dist/assembly/databasePlugins.d.cts +43 -0
  44. package/dist/assembly/databasePlugins.d.mts +43 -0
  45. package/dist/assembly/databasePlugins.mjs +22 -0
  46. package/dist/core/api.cjs +91 -0
  47. package/dist/core/api.d.cts +65 -0
  48. package/dist/core/api.d.mts +65 -0
  49. package/dist/core/api.mjs +87 -0
  50. package/dist/core/baseCandidates.cjs +71 -0
  51. package/dist/core/baseCandidates.d.cts +17 -0
  52. package/dist/core/baseCandidates.d.mts +17 -0
  53. package/dist/core/baseCandidates.mjs +69 -0
  54. package/dist/core/operations.cjs +350 -0
  55. package/dist/core/operations.mjs +350 -0
  56. package/dist/core/reads.cjs +273 -0
  57. package/dist/core/reads.d.cts +48 -0
  58. package/dist/core/reads.d.mts +48 -0
  59. package/dist/core/reads.mjs +267 -0
  60. package/dist/core/releases.cjs +193 -0
  61. package/dist/core/releases.mjs +190 -0
  62. package/dist/core/schema.cjs +279 -0
  63. package/dist/core/schema.d.cts +307 -0
  64. package/dist/core/schema.d.mts +307 -0
  65. package/dist/core/schema.mjs +277 -0
  66. package/dist/core/writes.cjs +96 -0
  67. package/dist/core/writes.mjs +91 -0
  68. package/dist/createHotUpdaterCore.cjs +61 -63
  69. package/dist/createHotUpdaterCore.d.cts +53 -45
  70. package/dist/createHotUpdaterCore.d.mts +53 -45
  71. package/dist/createHotUpdaterCore.mjs +61 -63
  72. package/dist/database/cursor.cjs +44 -0
  73. package/dist/database/cursor.d.cts +7 -0
  74. package/dist/database/cursor.d.mts +7 -0
  75. package/dist/database/cursor.mjs +40 -0
  76. package/dist/database/database.cjs +44 -0
  77. package/dist/database/database.d.cts +123 -0
  78. package/dist/database/database.d.mts +123 -0
  79. package/dist/database/database.mjs +44 -0
  80. package/dist/database/definitions.d.cts +68 -0
  81. package/dist/database/definitions.d.mts +68 -0
  82. package/dist/database/engine.cjs +37 -0
  83. package/dist/database/engine.d.cts +40 -0
  84. package/dist/database/engine.d.mts +40 -0
  85. package/dist/database/engine.mjs +37 -0
  86. package/dist/database/engineAggregates.cjs +115 -0
  87. package/dist/database/engineAggregates.mjs +112 -0
  88. package/dist/database/engineReads.cjs +180 -0
  89. package/dist/database/engineReads.d.cts +35 -0
  90. package/dist/database/engineReads.d.mts +35 -0
  91. package/dist/database/engineReads.mjs +179 -0
  92. package/dist/database/engineTransaction.cjs +403 -0
  93. package/dist/database/engineTransaction.d.cts +26 -0
  94. package/dist/database/engineTransaction.d.mts +26 -0
  95. package/dist/database/engineTransaction.mjs +402 -0
  96. package/dist/database/errors.cjs +27 -0
  97. package/dist/database/errors.d.cts +21 -0
  98. package/dist/database/errors.d.mts +21 -0
  99. package/dist/database/errors.mjs +24 -0
  100. package/dist/database/fence.cjs +124 -0
  101. package/dist/database/fence.d.cts +42 -0
  102. package/dist/database/fence.d.mts +42 -0
  103. package/dist/database/fence.mjs +116 -0
  104. package/dist/database/index.cjs +127 -0
  105. package/dist/database/index.d.cts +17 -0
  106. package/dist/database/index.d.mts +17 -0
  107. package/dist/database/index.mjs +15 -0
  108. package/dist/database/kv/kvAdapter.cjs +273 -0
  109. package/dist/database/kv/kvAdapter.d.cts +101 -0
  110. package/dist/database/kv/kvAdapter.d.mts +101 -0
  111. package/dist/database/kv/kvAdapter.mjs +272 -0
  112. package/dist/database/resolveSchema.cjs +196 -0
  113. package/dist/database/resolveSchema.d.cts +42 -0
  114. package/dist/database/resolveSchema.d.mts +42 -0
  115. package/dist/database/resolveSchema.mjs +193 -0
  116. package/dist/database/schema.cjs +21 -0
  117. package/dist/database/schema.d.cts +61 -0
  118. package/dist/database/schema.d.mts +61 -0
  119. package/dist/database/schema.mjs +20 -0
  120. package/dist/database/sql/sqlAdapter.cjs +282 -0
  121. package/dist/database/sql/sqlAdapter.d.cts +46 -0
  122. package/dist/database/sql/sqlAdapter.d.mts +46 -0
  123. package/dist/database/sql/sqlAdapter.mjs +279 -0
  124. package/dist/database/sql/sqlSchema.cjs +115 -0
  125. package/dist/database/sql/sqlSchema.d.cts +8 -0
  126. package/dist/database/sql/sqlSchema.d.mts +8 -0
  127. package/dist/database/sql/sqlSchema.mjs +111 -0
  128. package/dist/db/builtInDatabase.cjs +104 -0
  129. package/dist/db/builtInDatabase.d.cts +38 -0
  130. package/dist/db/builtInDatabase.d.mts +38 -0
  131. package/dist/db/builtInDatabase.mjs +97 -0
  132. package/dist/db/createBundleDiff.cjs +6 -6
  133. package/dist/db/createBundleDiff.d.cts +4 -7
  134. package/dist/db/createBundleDiff.d.mts +4 -7
  135. package/dist/db/createBundleDiff.mjs +5 -5
  136. package/dist/db/engineDrizzleSchema.cjs +54 -0
  137. package/dist/db/engineDrizzleSchema.mjs +54 -0
  138. package/dist/db/engineMigrator.cjs +53 -0
  139. package/dist/db/engineMigrator.mjs +53 -0
  140. package/dist/db/enginePrismaSchema.cjs +85 -0
  141. package/dist/db/enginePrismaSchema.d.cts +5 -0
  142. package/dist/db/enginePrismaSchema.d.mts +5 -0
  143. package/dist/db/enginePrismaSchema.mjs +84 -0
  144. package/dist/db/engineSql.cjs +26 -0
  145. package/dist/db/engineSql.d.cts +17 -0
  146. package/dist/db/engineSql.d.mts +17 -0
  147. package/dist/db/engineSql.mjs +25 -0
  148. package/dist/db/engineSqlMigrator.cjs +66 -0
  149. package/dist/db/engineSqlMigrator.mjs +66 -0
  150. package/dist/db/fixedMigratorShared.cjs +0 -11
  151. package/dist/db/fixedMigratorShared.mjs +1 -8
  152. package/dist/db/index.cjs +23 -8
  153. package/dist/db/index.d.cts +10 -4
  154. package/dist/db/index.d.mts +10 -4
  155. package/dist/db/index.mjs +16 -8
  156. package/dist/db/releaseCatalog.cjs +20 -44
  157. package/dist/db/releaseCatalog.d.cts +2 -3
  158. package/dist/db/releaseCatalog.d.mts +2 -3
  159. package/dist/db/releaseCatalog.mjs +20 -44
  160. package/dist/db/schemaSettings.cjs +63 -0
  161. package/dist/db/schemaSettings.d.cts +12 -0
  162. package/dist/db/schemaSettings.d.mts +12 -0
  163. package/dist/db/schemaSettings.mjs +62 -0
  164. package/dist/db/settingsMigrator.cjs +92 -0
  165. package/dist/db/settingsMigrator.mjs +89 -0
  166. package/dist/db/types.cjs +3 -12
  167. package/dist/db/types.d.cts +26 -69
  168. package/dist/db/types.d.mts +26 -69
  169. package/dist/db/types.mjs +2 -11
  170. package/dist/db/updateArtifacts.cjs +38 -68
  171. package/dist/db/updateArtifacts.mjs +40 -69
  172. package/dist/handler.cjs +72 -43
  173. package/dist/handler.d.cts +3 -6
  174. package/dist/handler.d.mts +3 -6
  175. package/dist/handler.mjs +73 -43
  176. package/dist/handlerAdminRoutes.cjs +375 -0
  177. package/dist/handlerAdminRoutes.mjs +373 -0
  178. package/dist/handlerParameters.cjs +6 -11
  179. package/dist/handlerParameters.mjs +6 -10
  180. package/dist/handlerReleaseCatalogRoutes.cjs +11 -14
  181. package/dist/handlerReleaseCatalogRoutes.mjs +11 -13
  182. package/dist/handlerTypes.d.cts +2 -56
  183. package/dist/handlerTypes.d.mts +2 -56
  184. package/dist/handlerVersionRoutes.cjs +20 -4
  185. package/dist/handlerVersionRoutes.d.cts +1 -0
  186. package/dist/handlerVersionRoutes.d.mts +1 -0
  187. package/dist/handlerVersionRoutes.mjs +20 -4
  188. package/dist/index.cjs +4 -7
  189. package/dist/index.d.cts +10 -9
  190. package/dist/index.d.mts +10 -9
  191. package/dist/index.mjs +4 -4
  192. package/dist/insights/domain.d.cts +53 -70
  193. package/dist/insights/domain.d.mts +53 -70
  194. package/dist/insights/errors.cjs +35 -8
  195. package/dist/insights/errors.mjs +35 -8
  196. package/dist/insights/eventInput.cjs +64 -24
  197. package/dist/insights/eventInput.mjs +63 -25
  198. package/dist/insights/provider.cjs +356 -0
  199. package/dist/insights/provider.d.cts +6 -0
  200. package/dist/insights/provider.d.mts +6 -0
  201. package/dist/insights/provider.mjs +355 -0
  202. package/dist/insights/queryInput.cjs +78 -43
  203. package/dist/insights/queryInput.mjs +76 -40
  204. package/dist/insights/routes.cjs +94 -34
  205. package/dist/insights/routes.mjs +93 -35
  206. package/dist/insights/types.d.cts +27 -12
  207. package/dist/insights/types.d.mts +27 -12
  208. package/dist/node.d.cts +1 -2
  209. package/dist/node.d.mts +1 -2
  210. package/dist/package.cjs +1 -1
  211. package/dist/package.mjs +1 -1
  212. package/dist/plugins/api-keys/index.cjs +50 -0
  213. package/dist/plugins/api-keys/index.d.cts +85 -0
  214. package/dist/plugins/api-keys/index.d.mts +85 -0
  215. package/dist/plugins/api-keys/index.mjs +47 -0
  216. package/dist/plugins/api-keys/model.cjs +58 -0
  217. package/dist/plugins/api-keys/model.d.cts +8 -0
  218. package/dist/plugins/api-keys/model.d.mts +8 -0
  219. package/dist/plugins/api-keys/model.mjs +58 -0
  220. package/dist/plugins/api-keys/schema.cjs +38 -0
  221. package/dist/plugins/api-keys/schema.d.cts +44 -0
  222. package/dist/plugins/api-keys/schema.d.mts +44 -0
  223. package/dist/plugins/api-keys/schema.mjs +39 -0
  224. package/dist/plugins/builtIn.cjs +7 -0
  225. package/dist/plugins/builtIn.mjs +6 -0
  226. package/dist/plugins/definePlugin.cjs +5 -0
  227. package/dist/plugins/definePlugin.d.cts +73 -0
  228. package/dist/plugins/definePlugin.d.mts +73 -0
  229. package/dist/plugins/definePlugin.mjs +5 -0
  230. package/dist/plugins/index.cjs +17 -0
  231. package/dist/plugins/index.d.cts +9 -0
  232. package/dist/plugins/index.d.mts +9 -0
  233. package/dist/plugins/index.mjs +7 -0
  234. package/dist/plugins/insights/index.cjs +48 -0
  235. package/dist/plugins/insights/index.d.cts +295 -0
  236. package/dist/plugins/insights/index.d.mts +295 -0
  237. package/dist/plugins/insights/index.mjs +44 -0
  238. package/dist/plugins/insights/model.cjs +17 -0
  239. package/dist/plugins/insights/model.d.cts +10 -0
  240. package/dist/plugins/insights/model.d.mts +10 -0
  241. package/dist/plugins/insights/model.mjs +17 -0
  242. package/dist/plugins/insights/reads.cjs +471 -0
  243. package/dist/plugins/insights/reads.mjs +466 -0
  244. package/dist/plugins/insights/recordEvent.cjs +135 -0
  245. package/dist/plugins/insights/recordEvent.d.cts +13 -0
  246. package/dist/plugins/insights/recordEvent.d.mts +13 -0
  247. package/dist/plugins/insights/recordEvent.mjs +132 -0
  248. package/dist/plugins/insights/schema.cjs +347 -0
  249. package/dist/plugins/insights/schema.d.cts +258 -0
  250. package/dist/plugins/insights/schema.d.mts +258 -0
  251. package/dist/plugins/insights/schema.mjs +344 -0
  252. package/dist/storageAccess.cjs +0 -1
  253. package/dist/types/index.d.cts +2 -29
  254. package/dist/types/index.d.mts +2 -29
  255. package/package.json +30 -7
  256. package/src/adapters/drizzle.conformance.integration.spec.ts +34 -0
  257. package/src/adapters/drizzle.readBudgets.integration.spec.ts +41 -0
  258. package/src/adapters/drizzle.spec.ts +277 -208
  259. package/src/adapters/drizzle.ts +48 -128
  260. package/src/adapters/drizzleExecutor.ts +166 -0
  261. package/src/adapters/kysely.conformance.integration.spec.ts +36 -0
  262. package/src/adapters/kysely.readBudgets.integration.spec.ts +42 -0
  263. package/src/adapters/kysely.spec.ts +212 -117
  264. package/src/adapters/kysely.ts +32 -87
  265. package/src/adapters/kyselyExecutor.ts +53 -0
  266. package/src/adapters/mongodb.integration.spec.ts +180 -0
  267. package/src/adapters/mongodb.readBudgets.integration.spec.ts +61 -0
  268. package/src/adapters/mongodb.ts +36 -65
  269. package/src/adapters/mongodbAdapter.ts +293 -0
  270. package/src/adapters/prisma.conformance.integration.spec.ts +36 -0
  271. package/src/adapters/prisma.integration.spec.ts +138 -0
  272. package/src/adapters/prisma.readBudgets.integration.spec.ts +40 -0
  273. package/src/adapters/prisma.spec.ts +226 -173
  274. package/src/adapters/prisma.ts +60 -451
  275. package/src/adapters/prismaExecutor.ts +107 -0
  276. package/src/adapters/prismaTestClients.ts +175 -0
  277. package/src/adapters/sqlProviders.ts +14 -0
  278. package/src/apiKeys.integration.spec.ts +19 -119
  279. package/src/apiKeys.spec.ts +39 -35
  280. package/src/apiKeys.ts +2 -8
  281. package/src/assembly/assemblePlugins.ts +269 -0
  282. package/src/assembly/assembly.spec.ts +325 -0
  283. package/src/assembly/assembly.types.spec.ts +155 -0
  284. package/src/assembly/builtInRoutes.spec.ts +213 -0
  285. package/src/assembly/databasePlugins.spec.ts +71 -0
  286. package/src/assembly/databasePlugins.ts +96 -0
  287. package/src/core/api.ts +139 -0
  288. package/src/core/baseCandidates.spec.ts +378 -0
  289. package/src/core/baseCandidates.ts +136 -0
  290. package/src/core/index.ts +44 -0
  291. package/src/core/operations.spec.ts +415 -0
  292. package/src/core/operations.ts +551 -0
  293. package/src/core/reads.spec.ts +300 -0
  294. package/src/core/reads.ts +417 -0
  295. package/src/core/releases.spec.ts +308 -0
  296. package/src/core/releases.ts +345 -0
  297. package/src/core/schema.ts +173 -0
  298. package/src/core/writes.ts +160 -0
  299. package/src/createHotUpdaterCore.ts +191 -167
  300. package/src/database/cursor.ts +65 -0
  301. package/src/database/database.ts +335 -0
  302. package/src/database/database.types.spec.ts +135 -0
  303. package/src/database/definitions.ts +72 -0
  304. package/src/database/engine.ts +62 -0
  305. package/src/database/engineAggregates.spec.ts +539 -0
  306. package/src/database/engineAggregates.ts +193 -0
  307. package/src/database/engineReads.spec.ts +219 -0
  308. package/src/database/engineReads.ts +302 -0
  309. package/src/database/engineTransaction.spec.ts +710 -0
  310. package/src/database/engineTransaction.ts +588 -0
  311. package/src/database/errors.ts +30 -0
  312. package/src/database/fence.spec.ts +223 -0
  313. package/src/database/fence.ts +171 -0
  314. package/src/database/index.ts +154 -0
  315. package/src/database/kv/kvAdapter.conformance.integration.spec.ts +17 -0
  316. package/src/database/kv/kvAdapter.readBudgets.integration.spec.ts +17 -0
  317. package/src/database/kv/kvAdapter.spec.ts +468 -0
  318. package/src/database/kv/kvAdapter.ts +450 -0
  319. package/src/database/kv/kvTestStore.ts +91 -0
  320. package/src/database/memory.readBudgets.integration.spec.ts +13 -0
  321. package/src/database/resolveSchema.spec.ts +230 -0
  322. package/src/database/resolveSchema.ts +372 -0
  323. package/src/database/schema.ts +169 -0
  324. package/src/database/schema.types.spec.ts +94 -0
  325. package/src/database/sql/docker-compose.yml +36 -0
  326. package/src/database/sql/sqlAdapter.integration.spec.ts +148 -0
  327. package/src/database/sql/sqlAdapter.readBudgets.integration.spec.ts +109 -0
  328. package/src/database/sql/sqlAdapter.spec.ts +255 -0
  329. package/src/database/sql/sqlAdapter.ts +532 -0
  330. package/src/database/sql/sqlSchema.ts +205 -0
  331. package/src/database/sql/sqlTestExecutors.ts +183 -0
  332. package/src/databaseTestSuite.spec.ts +39 -0
  333. package/src/db/builtInDatabase.ts +149 -0
  334. package/src/db/bundleRows.spec.ts +3 -3
  335. package/src/db/createBundleDiff.spec.ts +43 -29
  336. package/src/db/createBundleDiff.ts +13 -7
  337. package/src/db/engineDrizzleSchema.ts +72 -0
  338. package/src/db/engineMigrator.ts +105 -0
  339. package/src/db/enginePrismaSchema.ts +147 -0
  340. package/src/db/engineSql.spec.ts +129 -0
  341. package/src/db/engineSql.ts +48 -0
  342. package/src/db/engineSqlMigrator.ts +116 -0
  343. package/src/db/fixedMigratorShared.ts +1 -20
  344. package/src/db/index.spec.ts +116 -824
  345. package/src/db/index.ts +43 -14
  346. package/src/db/pluginTables.spec.ts +218 -0
  347. package/src/db/releaseCatalog.ts +48 -90
  348. package/src/db/schemaSettings.ts +104 -0
  349. package/src/db/settingsMigrator.spec.ts +65 -0
  350. package/src/db/settingsMigrator.ts +152 -0
  351. package/src/db/types.ts +25 -165
  352. package/src/db/updateArtifacts.spec.ts +170 -294
  353. package/src/db/updateArtifacts.ts +74 -142
  354. package/src/db/updateCheck.bench.ts +47 -0
  355. package/src/handler-standalone.integration.spec.ts +204 -412
  356. package/src/handler.spec.ts +134 -51
  357. package/src/handler.testFixtures.ts +14 -19
  358. package/src/handler.ts +163 -75
  359. package/src/handlerAdminRoutes.spec.ts +183 -0
  360. package/src/handlerAdminRoutes.ts +536 -0
  361. package/src/handlerAuth.integration.spec.ts +3 -3
  362. package/src/handlerManagement.spec.ts +136 -196
  363. package/src/handlerReleaseCatalogRoutes.ts +15 -17
  364. package/src/handlerTypes.ts +3 -80
  365. package/src/handlerVersionRoutes.ts +20 -7
  366. package/src/index.ts +7 -14
  367. package/src/insights/domain.ts +56 -77
  368. package/src/insights/errors.spec.ts +48 -0
  369. package/src/insights/errors.ts +50 -6
  370. package/src/insights/eventInput.ts +80 -27
  371. package/src/insights/insights.integration.spec.ts +525 -44
  372. package/src/insights/provider.spec.ts +574 -0
  373. package/src/insights/provider.ts +667 -0
  374. package/src/insights/queryInput.ts +113 -73
  375. package/src/insights/routes.ts +125 -69
  376. package/src/insights/types.ts +47 -36
  377. package/src/lintRules.spec.ts +76 -0
  378. package/src/plugins/api-keys/apiKeysPlugin.spec.ts +172 -0
  379. package/src/plugins/api-keys/index.ts +66 -0
  380. package/src/plugins/api-keys/model.ts +69 -0
  381. package/src/plugins/api-keys/schema.ts +22 -0
  382. package/src/plugins/builtIn.ts +5 -0
  383. package/src/plugins/definePlugin.ts +100 -0
  384. package/src/plugins/index.ts +55 -0
  385. package/src/plugins/insights/index.ts +86 -0
  386. package/src/plugins/insights/insightsRollout.integration.spec.ts +187 -0
  387. package/src/plugins/insights/model.spec.ts +66 -0
  388. package/src/plugins/insights/model.ts +19 -0
  389. package/src/plugins/insights/reads.spec.ts +725 -0
  390. package/src/plugins/insights/reads.ts +707 -0
  391. package/src/plugins/insights/recordEvent.spec.ts +443 -0
  392. package/src/plugins/insights/recordEvent.ts +238 -0
  393. package/src/plugins/insights/schema.ts +229 -0
  394. package/src/plugins/pluginTestHarness.spec.ts +56 -0
  395. package/src/readBudgets.testFixtures.ts +15 -0
  396. package/src/releaseCatalogRoutes.integration.spec.ts +47 -92
  397. package/src/runtime.spec.ts +91 -77
  398. package/src/runtime.testFixtures.ts +28 -44
  399. package/src/runtimeBundles.integration.spec.ts +112 -0
  400. package/src/testUtilsMutation.integration.spec.ts +215 -0
  401. package/src/testUtilsPackage.integration.spec.ts +176 -0
  402. package/src/types/index.ts +0 -32
  403. package/dist/adapters/databaseConstraintErrors.cjs +0 -23
  404. package/dist/adapters/databaseConstraintErrors.mjs +0 -21
  405. package/dist/adapters/databasePluginUtils.cjs +0 -117
  406. package/dist/adapters/databasePluginUtils.mjs +0 -106
  407. package/dist/adapters/drizzleCrud.cjs +0 -250
  408. package/dist/adapters/drizzleCrud.mjs +0 -249
  409. package/dist/adapters/drizzleLazyDB.cjs +0 -101
  410. package/dist/adapters/drizzleLazyDB.mjs +0 -101
  411. package/dist/adapters/drizzleQuery.cjs +0 -68
  412. package/dist/adapters/drizzleQuery.mjs +0 -67
  413. package/dist/adapters/kyselyCrud.cjs +0 -254
  414. package/dist/adapters/kyselyCrud.mjs +0 -253
  415. package/dist/adapters/kyselyQuery.cjs +0 -56
  416. package/dist/adapters/kyselyQuery.mjs +0 -55
  417. package/dist/adapters/mongodbCollections.cjs +0 -36
  418. package/dist/adapters/mongodbCollections.mjs +0 -30
  419. package/dist/adapters/mongodbQuery.cjs +0 -77
  420. package/dist/adapters/mongodbQuery.mjs +0 -70
  421. package/dist/adapters/mongodbReads.cjs +0 -148
  422. package/dist/adapters/mongodbReads.mjs +0 -147
  423. package/dist/adapters/mongodbWrites.cjs +0 -134
  424. package/dist/adapters/mongodbWrites.mjs +0 -133
  425. package/dist/adapters/prismaQuery.cjs +0 -58
  426. package/dist/adapters/prismaQuery.mjs +0 -56
  427. package/dist/adapters/prismaRows.cjs +0 -198
  428. package/dist/adapters/prismaRows.mjs +0 -188
  429. package/dist/db/databasePluginCore.cjs +0 -138
  430. package/dist/db/databasePluginCore.mjs +0 -137
  431. package/dist/db/fixedMigrator.cjs +0 -79
  432. package/dist/db/fixedMigrator.mjs +0 -78
  433. package/dist/db/fixedMigratorMongo.cjs +0 -319
  434. package/dist/db/fixedMigratorMongo.mjs +0 -319
  435. package/dist/db/mongoMigrationExecution.cjs +0 -11
  436. package/dist/db/mongoMigrationExecution.mjs +0 -11
  437. package/dist/db/schema/mongodb.cjs +0 -25
  438. package/dist/db/schema/mongodb.mjs +0 -25
  439. package/dist/db/schema/registry.cjs +0 -21
  440. package/dist/db/schema/registry.mjs +0 -17
  441. package/dist/db/schema/sql.cjs +0 -101
  442. package/dist/db/schema/sql.mjs +0 -97
  443. package/dist/db/schema/sqlOperations.cjs +0 -27
  444. package/dist/db/schema/sqlOperations.mjs +0 -26
  445. package/dist/db/schemaGenerators.cjs +0 -256
  446. package/dist/db/schemaGenerators.mjs +0 -254
  447. package/dist/db/schemaReadiness.cjs +0 -24
  448. package/dist/db/schemaReadiness.d.cts +0 -8
  449. package/dist/db/schemaReadiness.d.mts +0 -8
  450. package/dist/db/schemaReadiness.mjs +0 -23
  451. package/dist/db/sqlMigrationExecution.cjs +0 -67
  452. package/dist/db/sqlMigrationExecution.mjs +0 -66
  453. package/dist/handlerBundleRoutes.cjs +0 -155
  454. package/dist/handlerBundleRoutes.mjs +0 -155
  455. package/dist/handlerReleaseManagementRoutes.cjs +0 -125
  456. package/dist/handlerReleaseManagementRoutes.mjs +0 -124
  457. package/dist/insights/bounded/activeOverview.cjs +0 -87
  458. package/dist/insights/bounded/activeOverview.mjs +0 -85
  459. package/dist/insights/bounded/installationSearch.cjs +0 -42
  460. package/dist/insights/bounded/installationSearch.mjs +0 -42
  461. package/dist/insights/bounded/persistence.cjs +0 -39
  462. package/dist/insights/bounded/persistence.mjs +0 -38
  463. package/dist/insights/bounded/provider.cjs +0 -170
  464. package/dist/insights/bounded/provider.d.cts +0 -7
  465. package/dist/insights/bounded/provider.d.mts +0 -7
  466. package/dist/insights/bounded/provider.mjs +0 -170
  467. package/dist/insights/bounded/scan.cjs +0 -144
  468. package/dist/insights/bounded/scan.mjs +0 -138
  469. package/dist/insights/persistence.d.cts +0 -6
  470. package/dist/insights/persistence.d.mts +0 -6
  471. package/dist/schema/dsl.cjs +0 -103
  472. package/dist/schema/dsl.mjs +0 -87
  473. package/dist/schema/index.cjs +0 -6
  474. package/dist/schema/index.mjs +0 -7
  475. package/dist/schema/settings.cjs +0 -9
  476. package/dist/schema/settings.mjs +0 -9
  477. package/dist/schema/types.cjs +0 -8
  478. package/dist/schema/types.mjs +0 -6
  479. package/dist/schema/v1_0_0.cjs +0 -364
  480. package/dist/schema/v1_0_0.mjs +0 -357
  481. package/src/adapters/databaseConstraintErrors.ts +0 -25
  482. package/src/adapters/databasePluginTestDatabase.ts +0 -106
  483. package/src/adapters/databasePluginUtils.spec.ts +0 -29
  484. package/src/adapters/databasePluginUtils.ts +0 -214
  485. package/src/adapters/drizzleCrud.ts +0 -419
  486. package/src/adapters/drizzleLazyDB.ts +0 -257
  487. package/src/adapters/drizzleQuery.ts +0 -164
  488. package/src/adapters/kyselyCrud.ts +0 -588
  489. package/src/adapters/kyselyQuery.ts +0 -128
  490. package/src/adapters/mongodb.spec.ts +0 -141
  491. package/src/adapters/mongodbCollections.ts +0 -62
  492. package/src/adapters/mongodbQuery.ts +0 -226
  493. package/src/adapters/mongodbReads.ts +0 -311
  494. package/src/adapters/mongodbRows.spec.ts +0 -64
  495. package/src/adapters/mongodbRows.ts +0 -118
  496. package/src/adapters/mongodbTestClient.ts +0 -342
  497. package/src/adapters/mongodbTestFilter.ts +0 -166
  498. package/src/adapters/mongodbWrites.ts +0 -260
  499. package/src/adapters/prismaQuery.ts +0 -129
  500. package/src/adapters/prismaRows.spec.ts +0 -98
  501. package/src/adapters/prismaRows.ts +0 -301
  502. package/src/adapters/prismaTestClient.ts +0 -396
  503. package/src/calculatePagination.ts +0 -34
  504. package/src/db/databasePluginCore.spec.ts +0 -62
  505. package/src/db/databasePluginCore.testFixtures.ts +0 -76
  506. package/src/db/databasePluginCore.ts +0 -185
  507. package/src/db/databasePluginUpdateCheck.bench.ts +0 -107
  508. package/src/db/fixedMigrator.spec.ts +0 -231
  509. package/src/db/fixedMigrator.ts +0 -147
  510. package/src/db/fixedMigratorMongo.spec.ts +0 -192
  511. package/src/db/fixedMigratorMongo.ts +0 -425
  512. package/src/db/hotUpdaterSchema.ts +0 -5
  513. package/src/db/mongoMigrationExecution.ts +0 -23
  514. package/src/db/schema/definitions.ts +0 -1
  515. package/src/db/schema/mongodb.ts +0 -40
  516. package/src/db/schema/registry.ts +0 -55
  517. package/src/db/schema/sql.ts +0 -241
  518. package/src/db/schema/sqlOperations.ts +0 -63
  519. package/src/db/schema/types.ts +0 -1
  520. package/src/db/schemaGenerators.ts +0 -466
  521. package/src/db/schemaReadiness.ts +0 -34
  522. package/src/db/sqlMigrationExecution.spec.ts +0 -65
  523. package/src/db/sqlMigrationExecution.ts +0 -108
  524. package/src/db/types.spec.ts +0 -55
  525. package/src/handlerBundleRoutes.ts +0 -259
  526. package/src/handlerReleaseManagementRoutes.ts +0 -213
  527. package/src/insights/bounded/activeOverview.spec.ts +0 -189
  528. package/src/insights/bounded/activeOverview.ts +0 -148
  529. package/src/insights/bounded/installationSearch.spec.ts +0 -130
  530. package/src/insights/bounded/installationSearch.ts +0 -69
  531. package/src/insights/bounded/persistence.ts +0 -43
  532. package/src/insights/bounded/provider.spec.ts +0 -219
  533. package/src/insights/bounded/provider.ts +0 -229
  534. package/src/insights/bounded/scan.ts +0 -235
  535. package/src/insights/persistence.ts +0 -10
  536. package/src/schema/dsl-all-versions.spec.ts +0 -35
  537. package/src/schema/dsl.ts +0 -187
  538. package/src/schema/index.ts +0 -10
  539. package/src/schema/settings.ts +0 -15
  540. package/src/schema/types.ts +0 -79
  541. package/src/schema/v1_0_0.spec.ts +0 -146
  542. package/src/schema/v1_0_0.ts +0 -465
@@ -1,9 +1,25 @@
1
+ import {
2
+ createMemoryAdapter,
3
+ type DatabaseAdapter,
4
+ } from "@hot-updater/plugin-core/internal";
1
5
  import { afterEach, describe, expect, it, vi } from "vitest";
2
6
 
3
- import { createInMemoryDatabasePlugin } from "../../../test-utils/test/inMemoryDatabasePlugin";
7
+ import { builtInSettings } from "../db/builtInDatabase";
4
8
  import { createHotUpdater } from "../index";
5
- import { createSchemaManagedDatabase } from "../runtime.testFixtures";
6
- import { HOT_UPDATER_SCHEMA_VERSION } from "../schema/types";
9
+ import { insights } from "../plugins/insights";
10
+ import {
11
+ createFencedDatabase,
12
+ createRuntimeDatabase,
13
+ } from "../runtime.testFixtures";
14
+ import { EVENT_BODY_MAX_BYTES } from "./eventInput";
15
+
16
+ /** A server with the Insights plugin on an empty in-memory database. */
17
+ const start = () =>
18
+ createHotUpdater({
19
+ database: createRuntimeDatabase(),
20
+ plugins: [insights()],
21
+ clientAccess: "public",
22
+ });
7
23
 
8
24
  const event = {
9
25
  appVersion: "1.0.0",
@@ -18,6 +34,8 @@ const event = {
18
34
  toReleaseId: null,
19
35
  type: "UNCHANGED",
20
36
  updateStrategy: null,
37
+ userId: "user-1",
38
+ username: "Jane",
21
39
  sdkVersion: "2.0.0",
22
40
  } as const;
23
41
 
@@ -33,72 +51,282 @@ afterEach(() => {
33
51
  });
34
52
 
35
53
  describe("createHotUpdater Insights", () => {
36
- it("always persists events and serves queries without a server Insights flag", async () => {
54
+ it("ingests a downloaded bundle and exposes the running and pending state until apply", async () => {
55
+ const hotUpdater = start();
56
+ const downloaded = {
57
+ ...event,
58
+ type: "UPDATE_DOWNLOADED",
59
+ fromBundleId: "bundle-1",
60
+ toBundleId: "bundle-2",
61
+ toReleaseId: "release-2",
62
+ updateStrategy: "appVersion",
63
+ };
64
+ expect(
65
+ (await hotUpdater.handlers.client(eventRequest(downloaded))).status,
66
+ ).toBe(204);
67
+ const downloads = await hotUpdater.handlers.admin(
68
+ new Request(
69
+ `https://example.com/events?platform=ios&channel=production&bundleId=bundle-2&outcome=downloaded&beforeReceivedAtMs=${Date.now() + 1}`,
70
+ ),
71
+ );
72
+ expect(downloads.status).toBe(200);
73
+ await expect(downloads.json()).resolves.toMatchObject({
74
+ data: [{ type: "UPDATE_DOWNLOADED", toBundleId: "bundle-2" }],
75
+ });
76
+ const lookup = () =>
77
+ hotUpdater.handlers.admin(
78
+ new Request("https://example.com/installations/install-1"),
79
+ );
80
+ await expect((await lookup()).json()).resolves.toMatchObject({
81
+ latestStatus: "UPDATE_DOWNLOADED",
82
+ lastKnownBundleId: "bundle-1",
83
+ pendingBundleId: "bundle-2",
84
+ pendingReleaseId: "release-2",
85
+ });
86
+ expect(
87
+ (
88
+ await hotUpdater.handlers.client(
89
+ eventRequest({ ...downloaded, type: "UPDATE_APPLIED" }),
90
+ )
91
+ ).status,
92
+ ).toBe(204);
93
+ await expect((await lookup()).json()).resolves.toMatchObject({
94
+ latestStatus: "UPDATE_APPLIED",
95
+ lastKnownBundleId: "bundle-2",
96
+ pendingBundleId: null,
97
+ pendingReleaseId: null,
98
+ });
99
+ expect(
100
+ (
101
+ await hotUpdater.handlers.client(
102
+ eventRequest({ ...downloaded, fromBundleId: null }),
103
+ )
104
+ ).status,
105
+ ).toBe(400);
106
+ });
107
+
108
+ it("persists an event and serves the lean Insights views", async () => {
37
109
  vi.useFakeTimers();
38
110
  vi.setSystemTime(new Date("2026-08-12T00:00:00.000Z"));
39
- const database = createInMemoryDatabasePlugin();
40
- const append = vi.spyOn(database.models.insights, "append");
41
- const hotUpdater = createHotUpdater({
42
- database,
43
- clientAccess: { type: "public" },
44
- });
111
+ const receivedAtMs = Date.now();
112
+ const hotUpdater = start();
113
+ const append = vi.spyOn(hotUpdater.api.insights, "recordEvent");
45
114
 
46
115
  const ingestion = await hotUpdater.handlers.client(eventRequest());
47
116
  vi.advanceTimersByTime(1);
48
- const overview = await hotUpdater.handlers.admin(
49
- new Request("https://example.com/installations/overview"),
117
+ const events = await hotUpdater.handlers.admin(
118
+ new Request("https://example.com/events"),
119
+ );
120
+ const installation = await hotUpdater.handlers.admin(
121
+ new Request("https://example.com/installations/install-1"),
122
+ );
123
+ const matches = await hotUpdater.handlers.admin(
124
+ new Request("https://example.com/installations?userId=user-1"),
125
+ );
126
+ const active = await hotUpdater.handlers.admin(
127
+ new Request(
128
+ "https://example.com/overview?platform=ios&channel=production&window=24h",
129
+ ),
50
130
  );
51
131
 
52
132
  expect(ingestion.status).toBe(204);
53
133
  expect(append).toHaveBeenCalledWith(
54
134
  expect.objectContaining({
55
135
  install_id: "install-1",
56
- sdk_version: "2.0.0",
136
+ metadata: expect.objectContaining({ sdk_version: "2.0.0" }),
57
137
  to_bundle_id: "bundle-1",
58
138
  type: "UNCHANGED",
59
139
  }),
60
140
  );
61
- expect(hotUpdater.insights).toMatchObject({
62
- mode: "bounded",
63
- maxMatchingRows: 50_000,
141
+ expect(events.status).toBe(200);
142
+ await expect(events.json()).resolves.toMatchObject({
143
+ data: [
144
+ {
145
+ id: expect.any(String),
146
+ installId: "install-1",
147
+ receivedAtMs,
148
+ type: "UNCHANGED",
149
+ userId: "user-1",
150
+ username: "Jane",
151
+ },
152
+ ],
153
+ nextCursor: null,
154
+ });
155
+ expect(installation.status).toBe(200);
156
+ await expect(installation.json()).resolves.toMatchObject({
157
+ installId: "install-1",
158
+ latestStatus: "UNCHANGED",
159
+ userId: "user-1",
64
160
  });
161
+ expect(matches.status).toBe(200);
162
+ await expect(matches.json()).resolves.toMatchObject({
163
+ data: [{ installId: "install-1", userId: "user-1" }],
164
+ nextCursor: null,
165
+ });
166
+ expect(active.status).toBe(200);
167
+ // Whole hours, ending with the current one.
168
+ const end = Date.parse("2026-08-12T01:00:00.000Z");
169
+ await expect(active.json()).resolves.toEqual({
170
+ platform: "ios",
171
+ channel: "production",
172
+ sinceMs: end - 24 * 60 * 60 * 1_000,
173
+ beforeReceivedAtMs: end,
174
+ reportingInstallations: { count: 1, measuredAtMs: Date.now() },
175
+ window: "24h",
176
+ });
177
+ });
178
+
179
+ it("serves scoped bundle counts and the same half-open recovery drill-down", async () => {
180
+ vi.useFakeTimers();
181
+ const hotUpdater = start();
182
+ const report = async (
183
+ receivedAtMs: number,
184
+ overrides: Record<string, unknown>,
185
+ ) => {
186
+ vi.setSystemTime(receivedAtMs);
187
+ const response = await hotUpdater.handlers.client(
188
+ eventRequest({ ...event, ...overrides }),
189
+ );
190
+ expect(response.status).toBe(204);
191
+ };
192
+ await report(100, {
193
+ type: "UPDATE_APPLIED",
194
+ fromBundleId: "A",
195
+ toBundleId: "B",
196
+ updateStrategy: "appVersion",
197
+ });
198
+ await report(200, {
199
+ type: "RECOVERED",
200
+ fromBundleId: "B",
201
+ toBundleId: "A",
202
+ updateStrategy: "appVersion",
203
+ });
204
+ await report(300, {
205
+ installId: "install-2",
206
+ type: "UNCHANGED",
207
+ fromBundleId: null,
208
+ toBundleId: "B",
209
+ updateStrategy: null,
210
+ });
211
+ await report(400, {
212
+ installId: "install-3",
213
+ platform: "android",
214
+ toBundleId: "B",
215
+ });
216
+ vi.setSystemTime(500);
217
+ const overview = await hotUpdater.handlers.admin(
218
+ new Request(
219
+ "https://example.com/overview?platform=ios&channel=production&window=24h&bundleId=B",
220
+ ),
221
+ );
65
222
  expect(overview.status).toBe(200);
66
- await expect(overview.json()).resolves.toEqual({
67
- bundles: [{ bundleId: "bundle-1", installations: 1 }],
68
- trackedInstallations: 1,
223
+ await expect(overview.json()).resolves.toMatchObject({
224
+ sinceMs: 0,
225
+ beforeReceivedAtMs: 60 * 60 * 1_000,
226
+ reportingInstallations: { count: 2 },
227
+ bundle: {
228
+ bundleId: "B",
229
+ reportingInstallations: { count: 1 },
230
+ appliedReports: { count: 1 },
231
+ recoveredReports: { count: 1 },
232
+ unchangedReports: { count: 1 },
233
+ },
234
+ });
235
+ const drilldown = await hotUpdater.handlers.admin(
236
+ new Request(
237
+ "https://example.com/events?platform=ios&channel=production&bundleId=B&outcome=recovered&sinceMs=200&beforeReceivedAtMs=300",
238
+ ),
239
+ );
240
+ await expect(drilldown.json()).resolves.toMatchObject({
241
+ data: [
242
+ {
243
+ type: "RECOVERED",
244
+ fromBundleId: "B",
245
+ toBundleId: "A",
246
+ receivedAtMs: 200,
247
+ },
248
+ ],
249
+ nextCursor: null,
69
250
  });
251
+ const outside = await hotUpdater.handlers.admin(
252
+ new Request(
253
+ "https://example.com/events?platform=ios&channel=production&bundleId=B&outcome=recovered&sinceMs=0&beforeReceivedAtMs=200",
254
+ ),
255
+ );
256
+ await expect(outside.json()).resolves.toMatchObject({
257
+ data: [],
258
+ nextCursor: null,
259
+ });
260
+ });
261
+
262
+ it.each([
263
+ "/overview?window=24h",
264
+ "/overview?platform=ios&channel=production&platform=android",
265
+ "/events?bundleId=B",
266
+ "/events?platform=ios&channel=production&bundleId=B&outcome=UNCHANGED",
267
+ "/events?sinceMs=20&beforeReceivedAtMs=10",
268
+ // 90 days and 1 ms, longer than a bundle list covers.
269
+ "/events?platform=ios&channel=production&bundleId=B&outcome=applied&sinceMs=0&beforeReceivedAtMs=7776000001",
270
+ "/installations/install-1/events?platform=ios&channel=production&bundleId=B&outcome=applied",
271
+ ])("rejects ambiguous or invalid Insights query %s", async (path) => {
272
+ const hotUpdater = start();
273
+ expect(
274
+ (
275
+ await hotUpdater.handlers.admin(
276
+ new Request(`https://example.com${path}`),
277
+ )
278
+ ).status,
279
+ ).toBe(400);
70
280
  });
71
281
 
72
- it("checks the official schema before Insights reads and writes", async () => {
73
- const database = createSchemaManagedDatabase(
74
- "kysely",
75
- HOT_UPDATER_SCHEMA_VERSION,
282
+ it("lists at most 90 days of events, and says so for a longer range", async () => {
283
+ const hotUpdater = start();
284
+ const range = "sinceMs=0&beforeReceivedAtMs=7776000001";
285
+ const events = await hotUpdater.handlers.admin(
286
+ new Request(`https://example.com/events?${range}`),
76
287
  );
77
- const createMigrator = vi.spyOn(database, "createMigrator");
78
- const hotUpdater = createHotUpdater({
79
- database,
80
- clientAccess: { type: "public" },
288
+ expect(events.status).toBe(400);
289
+ await expect(events.json()).resolves.toEqual({
290
+ error:
291
+ "Insights event lists cover at most 90 days: send a sinceMs no more than 90 days before beforeReceivedAtMs.",
81
292
  });
293
+ // An installation's history is one index range, however long.
294
+ const history = await hotUpdater.handlers.admin(
295
+ new Request(
296
+ `https://example.com/installations/install-1/events?${range}`,
297
+ ),
298
+ );
299
+ expect(history.status).toBe(200);
300
+ });
82
301
 
83
- expect((await hotUpdater.handlers.client(eventRequest())).status).toBe(204);
84
- await hotUpdater.insights?.getBundleEventOverview();
302
+ it("checks the schema settings before Insights reads and writes", async () => {
303
+ const error = vi.spyOn(console, "error").mockImplementation(() => {});
304
+ const unmigrated = createHotUpdater({
305
+ database: await createFencedDatabase("kysely"),
306
+ plugins: [insights()],
307
+ clientAccess: "public",
308
+ });
309
+ const migrated = createHotUpdater({
310
+ database: await createFencedDatabase("kysely", builtInSettings),
311
+ plugins: [insights()],
312
+ clientAccess: "public",
313
+ });
85
314
 
86
- expect(createMigrator).toHaveBeenCalledOnce();
315
+ expect((await unmigrated.handlers.client(eventRequest())).status).toBe(503);
316
+ expect((await migrated.handlers.client(eventRequest())).status).toBe(204);
317
+ error.mockRestore();
87
318
  });
88
319
 
89
320
  it("keeps ingestion and queries on separate handler surfaces", async () => {
90
- const hotUpdater = createHotUpdater({
91
- database: createInMemoryDatabasePlugin(),
92
- clientAccess: { type: "public" },
93
- });
321
+ const hotUpdater = start();
94
322
 
95
323
  expect((await hotUpdater.handlers.client(eventRequest())).status).toBe(204);
96
324
  const clientQuery = await hotUpdater.handlers.client(
97
- new Request("https://example.com/installations/overview"),
325
+ new Request("https://example.com/events"),
98
326
  );
99
327
  const adminIngestion = await hotUpdater.handlers.admin(eventRequest());
100
328
  const adminQuery = await hotUpdater.handlers.admin(
101
- new Request("https://example.com/installations/overview"),
329
+ new Request("https://example.com/events"),
102
330
  );
103
331
 
104
332
  expect(clientQuery.status).toBe(404);
@@ -107,11 +335,41 @@ describe("createHotUpdater Insights", () => {
107
335
  expect(adminQuery.headers.get("cache-control")).toBe("private, no-store");
108
336
  });
109
337
 
338
+ it("records same-file selection as no change and rejects a fourth event type", async () => {
339
+ const hotUpdater = start();
340
+ const recordEvent = vi.spyOn(hotUpdater.api.insights, "recordEvent");
341
+ const selection = {
342
+ ...event,
343
+ fromReleaseId: "previous-release",
344
+ toReleaseId: "selected-release",
345
+ };
346
+ expect(
347
+ (await hotUpdater.handlers.client(eventRequest(selection))).status,
348
+ ).toBe(204);
349
+ expect(recordEvent).toHaveBeenCalledWith(
350
+ expect.objectContaining({
351
+ type: "UNCHANGED",
352
+ from_bundle_id: null,
353
+ metadata: expect.objectContaining({ update_strategy: null }),
354
+ from_release_id: "previous-release",
355
+ to_release_id: "selected-release",
356
+ to_bundle_id: event.toBundleId,
357
+ }),
358
+ );
359
+ const response = await hotUpdater.handlers.client(
360
+ eventRequest({
361
+ ...selection,
362
+ type: "RELEASE_ADOPTED",
363
+ fromBundleId: event.toBundleId,
364
+ updateStrategy: "appVersion",
365
+ }),
366
+ );
367
+ expect(response.status).toBe(400);
368
+ expect(recordEvent).toHaveBeenCalledTimes(1);
369
+ });
370
+
110
371
  it("returns a stable client error for malformed event payloads", async () => {
111
- const hotUpdater = createHotUpdater({
112
- database: createInMemoryDatabasePlugin(),
113
- clientAccess: { type: "public" },
114
- });
372
+ const hotUpdater = start();
115
373
 
116
374
  const response = await hotUpdater.handlers.client(
117
375
  eventRequest({ ...event, platform: "web" }),
@@ -123,13 +381,87 @@ describe("createHotUpdater Insights", () => {
123
381
  });
124
382
  });
125
383
 
384
+ it.each(["installId", "userId"] as const)(
385
+ "rejects a 256-character event %s",
386
+ async (field) => {
387
+ const hotUpdater = start();
388
+
389
+ const response = await hotUpdater.handlers.client(
390
+ eventRequest({ ...event, [field]: "x".repeat(256) }),
391
+ );
392
+
393
+ expect(response.status).toBe(400);
394
+ await expect(response.json()).resolves.toEqual({
395
+ error: `Invalid event field: ${field}`,
396
+ });
397
+ },
398
+ );
399
+
400
+ it("preserves a leading Unicode BOM in exact installation and current-user IDs", async () => {
401
+ const hotUpdater = start();
402
+ const installId = "\uFEFFinstall";
403
+ const userId = "\uFEFFuser";
404
+ expect(
405
+ (
406
+ await hotUpdater.handlers.client(
407
+ eventRequest({ ...event, installId, userId }),
408
+ )
409
+ ).status,
410
+ ).toBe(204);
411
+ const installation = await hotUpdater.handlers.admin(
412
+ new Request(
413
+ `https://example.com/installations/${encodeURIComponent(installId)}`,
414
+ ),
415
+ );
416
+ expect(installation.status).toBe(200);
417
+ await expect(installation.json()).resolves.toMatchObject({
418
+ installId,
419
+ userId,
420
+ });
421
+ const matches = await hotUpdater.handlers.admin(
422
+ new Request(
423
+ `https://example.com/installations?userId=${encodeURIComponent(userId)}`,
424
+ ),
425
+ );
426
+ expect(matches.status).toBe(200);
427
+ await expect(matches.json()).resolves.toMatchObject({
428
+ data: [{ installId, userId }],
429
+ });
430
+ });
431
+
432
+ it.each(["\uD800", "\uDC00"])(
433
+ "rejects an unmatched surrogate identity %j",
434
+ async (installId) => {
435
+ const hotUpdater = start();
436
+ expect(
437
+ (
438
+ await hotUpdater.handlers.client(
439
+ eventRequest({ ...event, installId }),
440
+ )
441
+ ).status,
442
+ ).toBe(400);
443
+ },
444
+ );
445
+
446
+ it("rejects 256-character installation query identities", async () => {
447
+ const hotUpdater = start();
448
+ const tooLong = "x".repeat(256);
449
+
450
+ const user = await hotUpdater.handlers.admin(
451
+ new Request(`https://example.com/installations?userId=${tooLong}`),
452
+ );
453
+ const installation = await hotUpdater.handlers.admin(
454
+ new Request(`https://example.com/installations/${tooLong}`),
455
+ );
456
+
457
+ expect(user.status).toBe(400);
458
+ expect(installation.status).toBe(400);
459
+ });
460
+
126
461
  it.each(["fromReleaseId", "toReleaseId"] as const)(
127
462
  "rejects an omitted %s instead of treating it as null",
128
463
  async (field) => {
129
- const hotUpdater = createHotUpdater({
130
- database: createInMemoryDatabasePlugin(),
131
- clientAccess: { type: "public" },
132
- });
464
+ const hotUpdater = start();
133
465
  const payload: Record<string, unknown> = { ...event };
134
466
  delete payload[field];
135
467
 
@@ -141,4 +473,153 @@ describe("createHotUpdater Insights", () => {
141
473
  });
142
474
  },
143
475
  );
476
+
477
+ it("records a report with fields it does not know and still checks the ones it does", async () => {
478
+ const hotUpdater = start();
479
+ const newer = { ...event, networkType: "wifi", screen: { width: 390 } };
480
+
481
+ expect((await hotUpdater.handlers.client(eventRequest(newer))).status).toBe(
482
+ 204,
483
+ );
484
+ const installation = await hotUpdater.handlers.admin(
485
+ new Request("https://example.com/installations/install-1"),
486
+ );
487
+ await expect(installation.json()).resolves.toMatchObject({
488
+ installId: "install-1",
489
+ latestStatus: "UNCHANGED",
490
+ });
491
+ const invalid = await hotUpdater.handlers.client(
492
+ eventRequest({ ...newer, channel: 7 }),
493
+ );
494
+ expect(invalid.status).toBe(400);
495
+ await expect(invalid.json()).resolves.toEqual({
496
+ error: "Invalid event field: channel",
497
+ });
498
+ const tooLarge = await hotUpdater.handlers.client(
499
+ eventRequest({ ...newer, padding: "x".repeat(EVENT_BODY_MAX_BYTES) }),
500
+ );
501
+ expect(tooLarge.status).toBe(413);
502
+ });
503
+
504
+ it("counts a retried report once under its client event ID, whichever installation repeats it", async () => {
505
+ const hotUpdater = start();
506
+ const eventId = "01987a6e-4c00-7abc-8def-0123456789ab";
507
+ const report = { ...event, eventId };
508
+ const counts = async () => {
509
+ const overview = await hotUpdater.handlers.admin(
510
+ new Request(
511
+ "https://example.com/overview?platform=ios&channel=production&window=24h&bundleId=bundle-1",
512
+ ),
513
+ );
514
+ return (
515
+ (await overview.json()) as {
516
+ readonly bundle: { readonly unchangedReports: { count: number } };
517
+ }
518
+ ).bundle.unchangedReports.count;
519
+ };
520
+
521
+ for (let attempt = 0; attempt < 2; attempt += 1) {
522
+ expect(
523
+ (await hotUpdater.handlers.client(eventRequest(report))).status,
524
+ ).toBe(204);
525
+ }
526
+ await expect(counts()).resolves.toBe(1);
527
+ const events = await hotUpdater.handlers.admin(
528
+ new Request(
529
+ `https://example.com/events?beforeReceivedAtMs=${Date.now() + 1}`,
530
+ ),
531
+ );
532
+ await expect(events.json()).resolves.toMatchObject({
533
+ data: [{ id: eventId, installId: "install-1" }],
534
+ });
535
+
536
+ // A report under a stored ID changes nothing, as analytics ingestion
537
+ // drops duplicates.
538
+ expect(
539
+ (
540
+ await hotUpdater.handlers.client(
541
+ eventRequest({ ...report, installId: "install-2" }),
542
+ )
543
+ ).status,
544
+ ).toBe(204);
545
+ expect(
546
+ (
547
+ await hotUpdater.handlers.admin(
548
+ new Request("https://example.com/installations/install-2"),
549
+ )
550
+ ).status,
551
+ ).toBe(404);
552
+
553
+ // Without an ID the server creates one, so each report counts.
554
+ for (let attempt = 0; attempt < 2; attempt += 1) {
555
+ expect(
556
+ (await hotUpdater.handlers.client(eventRequest(event))).status,
557
+ ).toBe(204);
558
+ }
559
+ await expect(counts()).resolves.toBe(3);
560
+ });
561
+
562
+ it.each([
563
+ "event-1",
564
+ "01987A6E-4C00-7ABC-8DEF-0123456789AB",
565
+ "01987a6e-4c00-4abc-8def-0123456789ab",
566
+ null,
567
+ ])("rejects the event ID %j", async (eventId) => {
568
+ const hotUpdater = start();
569
+
570
+ const response = await hotUpdater.handlers.client(
571
+ eventRequest({ ...event, eventId }),
572
+ );
573
+
574
+ expect(response.status).toBe(400);
575
+ await expect(response.json()).resolves.toEqual({
576
+ error: "Invalid event field: eventId",
577
+ });
578
+ });
579
+
580
+ it("answers 503 with Retry-After while the database is busy, and 500 when it fails", async () => {
581
+ const error = vi.spyOn(console, "error").mockImplementation(() => {});
582
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
583
+ const on = (overrides: Partial<DatabaseAdapter>) =>
584
+ createHotUpdater({
585
+ database: {
586
+ name: "testDatabase",
587
+ adapter: { ...createMemoryAdapter(), ...overrides },
588
+ },
589
+ plugins: [insights()],
590
+ clientAccess: "public",
591
+ }).handlers.client(eventRequest());
592
+ const throttled = Object.assign(
593
+ new Error("The level of configured provisioned throughput was exceeded."),
594
+ { name: "ProvisionedThroughputExceededException" },
595
+ );
596
+
597
+ // Every write is refused as transient until the engine runs out of retries.
598
+ const busy = await on({ write: async () => ({ ok: false, retry: true }) });
599
+ expect(busy.status).toBe(503);
600
+ expect(busy.headers.get("retry-after")).toBe("5");
601
+ await expect(busy.json()).resolves.toEqual({
602
+ error: "Service unavailable",
603
+ });
604
+ // A throttled read throws the backend's own error.
605
+ const throttledRead = await on({
606
+ get: async () => {
607
+ throw throttled;
608
+ },
609
+ });
610
+ expect(throttledRead.status).toBe(503);
611
+ expect(throttledRead.headers.get("retry-after")).toBe("5");
612
+ const failed = await on({
613
+ get: async () => {
614
+ throw new Error("connection refused");
615
+ },
616
+ });
617
+ expect(failed.status).toBe(500);
618
+ expect(failed.headers.get("retry-after")).toBeNull();
619
+ // Each 503 warns, and only the failure logs as an error.
620
+ expect(warn).toHaveBeenCalledTimes(2);
621
+ expect(error).toHaveBeenCalledTimes(1);
622
+ error.mockRestore();
623
+ warn.mockRestore();
624
+ });
144
625
  });