@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
@@ -0,0 +1,667 @@
1
+ import {
2
+ compareInsightsText,
3
+ isInsightsMovementEvent,
4
+ isUUIDv7,
5
+ type InsightsBundleEventFilter,
6
+ type BundleEventRow,
7
+ type InsightsEventCursor,
8
+ type InsightsEventFilter,
9
+ type InsightsModel,
10
+ } from "@hot-updater/plugin-core";
11
+
12
+ import type {
13
+ ActiveInstallationWindow,
14
+ InsightsBundleSelection,
15
+ InsightsScope,
16
+ EventHistoryRow,
17
+ InstallationHistoryRow,
18
+ InstallationRow,
19
+ } from "./domain";
20
+ import { InsightsBadRequestError } from "./errors";
21
+ import { createBundleEventRow } from "./eventInput";
22
+ import type {
23
+ InsightsEventPageInput,
24
+ InsightsInstallationEventPageInput,
25
+ InsightsProvider,
26
+ InsightsUserInstallationPageInput,
27
+ } from "./types";
28
+
29
+ const DEFAULT_PAGE_LIMIT = 50;
30
+ const MAX_PAGE_LIMIT = 100;
31
+ const MAX_EVENT_ID_LENGTH = 1_024;
32
+ const MAX_IDENTITY_LENGTH = 255;
33
+ const MAX_CURSOR_LENGTH = 8 * 1_024;
34
+
35
+ const HOUR_MS = 60 * 60 * 1_000;
36
+
37
+ const WINDOW_MS: Record<ActiveInstallationWindow, number> = {
38
+ "24h": 24 * 60 * 60 * 1_000,
39
+ "7d": 7 * 24 * 60 * 60 * 1_000,
40
+ "30d": 30 * 24 * 60 * 60 * 1_000,
41
+ };
42
+
43
+ /**
44
+ * The longest receipt range, `[sinceMs, beforeReceivedAtMs)`, of a global or
45
+ * bundle event list, and the range one covers without `sinceMs`: raw events
46
+ * are listed inside a time range, as analytics products list them. It is a
47
+ * duration, so a range that ends mid-day touches 91 UTC days. Installation
48
+ * history is one index range and reads back to `sinceMs`.
49
+ */
50
+ export const EVENT_LIST_RANGE_MS = 90 * 24 * HOUR_MS;
51
+
52
+ /** A full page's last row, and the range every page of the list shares. */
53
+ type EventCursorPayload = {
54
+ readonly after: InsightsEventCursor;
55
+ readonly beforeReceivedAtMs: number;
56
+ readonly sinceMs: number;
57
+ readonly kind: "events";
58
+ readonly filter: InsightsEventFilter;
59
+ readonly version: 2;
60
+ };
61
+
62
+ type UserInstallationCursorPayload = {
63
+ readonly afterInstallId: string;
64
+ readonly kind: "user-installations";
65
+ readonly userId: string;
66
+ readonly version: 1;
67
+ };
68
+
69
+ const isRecord = (value: unknown): value is Readonly<Record<string, unknown>> =>
70
+ typeof value === "object" && value !== null && !Array.isArray(value);
71
+
72
+ const requireString = (
73
+ value: unknown,
74
+ label: string,
75
+ maximumLength: number,
76
+ ): string => {
77
+ if (
78
+ typeof value !== "string" ||
79
+ value.length === 0 ||
80
+ value.length > maximumLength ||
81
+ new TextDecoder("utf-8", { ignoreBOM: true }).decode(
82
+ new TextEncoder().encode(value),
83
+ ) !== value
84
+ ) {
85
+ throw new InsightsBadRequestError(`Invalid ${label}.`);
86
+ }
87
+ return value;
88
+ };
89
+
90
+ const requireTimestamp = (value: unknown, label: string): number => {
91
+ if (!Number.isSafeInteger(value) || Number(value) < 0) {
92
+ throw new InsightsBadRequestError(`Invalid ${label}.`);
93
+ }
94
+ return Number(value);
95
+ };
96
+
97
+ const readLimit = (value: number | undefined): number => {
98
+ const limit = value ?? DEFAULT_PAGE_LIMIT;
99
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_PAGE_LIMIT) {
100
+ throw new InsightsBadRequestError("Invalid page limit.");
101
+ }
102
+ return limit;
103
+ };
104
+
105
+ const encodeBase64Url = (value: string): string => {
106
+ const bytes = new TextEncoder().encode(value);
107
+ let binary = "";
108
+ for (const byte of bytes) binary += String.fromCharCode(byte);
109
+ return btoa(binary)
110
+ .replaceAll("+", "-")
111
+ .replaceAll("/", "_")
112
+ .replace(/=+$/u, "");
113
+ };
114
+
115
+ const decodeBase64Url = (value: string): string => {
116
+ if (value.length === 0 || value.length > MAX_CURSOR_LENGTH) {
117
+ throw new InsightsBadRequestError("Invalid Insights cursor.");
118
+ }
119
+ try {
120
+ const base64 = value.replaceAll("-", "+").replaceAll("_", "/");
121
+ const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
122
+ const binary = atob(padded);
123
+ const bytes = Uint8Array.from(binary, (character) =>
124
+ character.charCodeAt(0),
125
+ );
126
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
127
+ } catch {
128
+ throw new InsightsBadRequestError("Invalid Insights cursor.");
129
+ }
130
+ };
131
+
132
+ const encodeCursor = (
133
+ value: EventCursorPayload | UserInstallationCursorPayload,
134
+ ): string => encodeBase64Url(JSON.stringify(value));
135
+
136
+ const decodeCursor = (value: string): unknown => {
137
+ try {
138
+ return JSON.parse(decodeBase64Url(value));
139
+ } catch (error) {
140
+ if (error instanceof InsightsBadRequestError) throw error;
141
+ throw new InsightsBadRequestError("Invalid Insights cursor.");
142
+ }
143
+ };
144
+
145
+ const readScope = (input: InsightsScope): InsightsScope => {
146
+ if (input.platform !== "ios" && input.platform !== "android") {
147
+ throw new InsightsBadRequestError("Invalid Insights platform.");
148
+ }
149
+ return {
150
+ platform: input.platform,
151
+ channel: requireString(input.channel, "channel", MAX_EVENT_ID_LENGTH),
152
+ };
153
+ };
154
+
155
+ // Counts and drill-downs share the exact same raw predicate.
156
+ const bundleFilter = (
157
+ input: InsightsBundleSelection,
158
+ ): InsightsBundleEventFilter => {
159
+ const scope = readScope(input);
160
+ const bundleId = requireString(
161
+ input.bundleId,
162
+ "bundle ID",
163
+ MAX_EVENT_ID_LENGTH,
164
+ );
165
+ switch (input.outcome) {
166
+ case "downloaded":
167
+ return { ...scope, type: "UPDATE_DOWNLOADED", toBundleId: bundleId };
168
+ case "applied":
169
+ return { ...scope, type: "UPDATE_APPLIED", toBundleId: bundleId };
170
+ case "recovered":
171
+ return { ...scope, type: "RECOVERED", fromBundleId: bundleId };
172
+ case "unchanged":
173
+ return { ...scope, type: "UNCHANGED", toBundleId: bundleId };
174
+ default:
175
+ throw new InsightsBadRequestError("Invalid Insights outcome.");
176
+ }
177
+ };
178
+
179
+ const sameFilter = (left: unknown, right: InsightsEventFilter): boolean => {
180
+ if (!isRecord(left) || left.kind !== right.kind) return false;
181
+ switch (right.kind) {
182
+ case "all":
183
+ return true;
184
+ case "installationMovement":
185
+ return left.installId === right.installId;
186
+ case "bundle":
187
+ return (
188
+ left.platform === right.platform &&
189
+ left.channel === right.channel &&
190
+ left.type === right.type &&
191
+ (right.type === "RECOVERED"
192
+ ? left.fromBundleId === right.fromBundleId
193
+ : left.toBundleId === right.toBundleId)
194
+ );
195
+ }
196
+ };
197
+
198
+ const readEventCursor = (
199
+ value: string,
200
+ filter: InsightsEventFilter,
201
+ ): EventCursorPayload => {
202
+ const cursor = decodeCursor(value);
203
+ // A cursor names the row it continues after. One that only held a time
204
+ // (`olderThanMs`) resumed below a window of days, which lists no longer do.
205
+ if (
206
+ !isRecord(cursor) ||
207
+ cursor.version !== 2 ||
208
+ cursor.kind !== "events" ||
209
+ !isRecord(cursor.filter) ||
210
+ !isRecord(cursor.after)
211
+ ) {
212
+ throw new InsightsBadRequestError("Invalid Insights cursor.");
213
+ }
214
+ if (!sameFilter(cursor.filter, filter)) {
215
+ throw new InsightsBadRequestError(
216
+ "Insights cursor does not match the requested events.",
217
+ );
218
+ }
219
+ if (typeof cursor.after.id !== "string" || !isUUIDv7(cursor.after.id)) {
220
+ throw new InsightsBadRequestError("Invalid Insights event cursor ID.");
221
+ }
222
+ return {
223
+ after: {
224
+ id: cursor.after.id,
225
+ receivedAtMs: requireTimestamp(cursor.after.receivedAtMs, "event cursor"),
226
+ },
227
+ beforeReceivedAtMs: requireTimestamp(
228
+ cursor.beforeReceivedAtMs,
229
+ "event cutoff",
230
+ ),
231
+ kind: "events",
232
+ sinceMs: requireTimestamp(cursor.sinceMs, "event start"),
233
+ filter,
234
+ version: 2,
235
+ };
236
+ };
237
+
238
+ const readUserInstallationCursor = (
239
+ value: string,
240
+ userId: string,
241
+ ): UserInstallationCursorPayload => {
242
+ const cursor = decodeCursor(value);
243
+ if (
244
+ !isRecord(cursor) ||
245
+ cursor.version !== 1 ||
246
+ cursor.kind !== "user-installations" ||
247
+ cursor.userId !== userId
248
+ ) {
249
+ throw new InsightsBadRequestError(
250
+ "Insights cursor does not match the requested user ID.",
251
+ );
252
+ }
253
+ return {
254
+ afterInstallId: requireString(
255
+ cursor.afterInstallId,
256
+ "installation cursor",
257
+ MAX_IDENTITY_LENGTH,
258
+ ),
259
+ kind: "user-installations",
260
+ userId,
261
+ version: 1,
262
+ };
263
+ };
264
+
265
+ const compareEventNewest = (
266
+ left: Pick<BundleEventRow, "id" | "received_at_ms">,
267
+ right: Pick<BundleEventRow, "id" | "received_at_ms">,
268
+ ): number =>
269
+ right.received_at_ms - left.received_at_ms ||
270
+ compareInsightsText(right.id, left.id);
271
+
272
+ const isAfterEventCursor = (
273
+ row: BundleEventRow,
274
+ after: InsightsEventCursor,
275
+ ): boolean =>
276
+ row.received_at_ms < after.receivedAtMs ||
277
+ (row.received_at_ms === after.receivedAtMs &&
278
+ compareInsightsText(row.id, after.id) < 0);
279
+
280
+ const assertEventRows = (
281
+ rows: readonly BundleEventRow[],
282
+ input: {
283
+ readonly after?: InsightsEventCursor;
284
+ readonly beforeReceivedAtMs: number;
285
+ readonly limit: number;
286
+ readonly filter: InsightsEventFilter;
287
+ readonly sinceMs: number;
288
+ },
289
+ ): void => {
290
+ if (rows.length > input.limit) {
291
+ throw new Error("Insights database returned too many event rows.");
292
+ }
293
+ for (let index = 0; index < rows.length; index += 1) {
294
+ const row = rows[index];
295
+ const previous = rows[index - 1];
296
+ if (
297
+ !row ||
298
+ typeof row.id !== "string" ||
299
+ !isUUIDv7(row.id) ||
300
+ !Number.isSafeInteger(row.received_at_ms) ||
301
+ row.received_at_ms >= input.beforeReceivedAtMs ||
302
+ row.received_at_ms < input.sinceMs ||
303
+ (previous !== undefined && compareEventNewest(previous, row) >= 0) ||
304
+ (input.after !== undefined && !isAfterEventCursor(row, input.after)) ||
305
+ (input.filter.kind === "installationMovement" &&
306
+ (row.install_id !== input.filter.installId ||
307
+ !isInsightsMovementEvent(row))) ||
308
+ (input.filter.kind === "bundle" &&
309
+ (row.platform !== input.filter.platform ||
310
+ row.channel !== input.filter.channel ||
311
+ row.type !== input.filter.type ||
312
+ (input.filter.type === "RECOVERED"
313
+ ? row.from_bundle_id !== input.filter.fromBundleId
314
+ : row.to_bundle_id !== input.filter.toBundleId)))
315
+ ) {
316
+ throw new Error("Insights database returned invalid event rows.");
317
+ }
318
+ }
319
+ };
320
+
321
+ const toEventHistoryRow = (row: BundleEventRow): EventHistoryRow => ({
322
+ appVersion: row.app_version,
323
+ channel: row.channel,
324
+ cohort: row.metadata.cohort,
325
+ fromBundleId: row.from_bundle_id,
326
+ id: row.id,
327
+ installId: row.install_id,
328
+ platform: row.platform,
329
+ receivedAtMs: row.received_at_ms,
330
+ toBundleId: row.to_bundle_id,
331
+ type: row.type,
332
+ userId: row.user_id,
333
+ username: row.metadata.username,
334
+ });
335
+
336
+ const toInstallationRow = (row: BundleEventRow): InstallationRow => ({
337
+ appVersion: row.app_version,
338
+ channel: row.channel,
339
+ cohort: row.metadata.cohort,
340
+ installId: row.install_id,
341
+ lastKnownBundleId:
342
+ row.type === "UPDATE_DOWNLOADED" ? row.from_bundle_id : row.to_bundle_id,
343
+ pendingBundleId: row.type === "UPDATE_DOWNLOADED" ? row.to_bundle_id : null,
344
+ pendingReleaseId: row.type === "UPDATE_DOWNLOADED" ? row.to_release_id : null,
345
+ latestStatus: row.type,
346
+ platform: row.platform,
347
+ receivedAtMs: row.received_at_ms,
348
+ userId: row.user_id,
349
+ username: row.metadata.username,
350
+ });
351
+
352
+ interface EventBounds {
353
+ readonly beforeReceivedAtMs: number;
354
+ readonly sinceMs: number;
355
+ }
356
+
357
+ /**
358
+ * Where a list without `sinceMs` starts: 90 days before its cutoff for the
359
+ * global and bundle lists, at the first event for installation history.
360
+ */
361
+ const defaultEventStart = (
362
+ filter: InsightsEventFilter,
363
+ beforeReceivedAtMs: number,
364
+ ): number =>
365
+ filter.kind === "installationMovement"
366
+ ? 0
367
+ : Math.max(0, beforeReceivedAtMs - EVENT_LIST_RANGE_MS);
368
+
369
+ /** Whether a cursor's row lies inside the list's range. */
370
+ const isInsideBounds = ({ after }: EventCursorPayload, bounds: EventBounds) =>
371
+ after.receivedAtMs >= bounds.sinceMs &&
372
+ after.receivedAtMs < bounds.beforeReceivedAtMs;
373
+
374
+ /** A page's range: its cursor's, which the request may repeat but not change. */
375
+ const readEventBounds = (
376
+ input: InsightsEventPageInput,
377
+ cursor: EventCursorPayload | undefined,
378
+ filter: InsightsEventFilter,
379
+ ): EventBounds => {
380
+ const beforeReceivedAtMs =
381
+ cursor?.beforeReceivedAtMs ??
382
+ (input.beforeReceivedAtMs === undefined
383
+ ? Date.now()
384
+ : requireTimestamp(input.beforeReceivedAtMs, "event cutoff"));
385
+ if (
386
+ cursor !== undefined &&
387
+ input.beforeReceivedAtMs !== undefined &&
388
+ input.beforeReceivedAtMs !== beforeReceivedAtMs
389
+ ) {
390
+ throw new InsightsBadRequestError(
391
+ "Insights cursor does not match the requested event cutoff.",
392
+ );
393
+ }
394
+ const sinceMs =
395
+ cursor?.sinceMs ??
396
+ (input.sinceMs === undefined
397
+ ? defaultEventStart(filter, beforeReceivedAtMs)
398
+ : requireTimestamp(input.sinceMs, "event start"));
399
+ const bounds = { beforeReceivedAtMs, sinceMs };
400
+ if (
401
+ sinceMs > beforeReceivedAtMs ||
402
+ (input.sinceMs !== undefined && input.sinceMs !== sinceMs) ||
403
+ (cursor !== undefined && !isInsideBounds(cursor, bounds))
404
+ ) {
405
+ throw new InsightsBadRequestError(
406
+ "Insights cursor or range does not match the requested event start.",
407
+ );
408
+ }
409
+ if (
410
+ filter.kind !== "installationMovement" &&
411
+ beforeReceivedAtMs - sinceMs > EVENT_LIST_RANGE_MS
412
+ ) {
413
+ throw new InsightsBadRequestError(
414
+ "Insights event lists cover at most 90 days: send a sinceMs no more than 90 days before beforeReceivedAtMs.",
415
+ );
416
+ }
417
+ return bounds;
418
+ };
419
+
420
+ const pageEventRows = async <T extends EventHistoryRow>(
421
+ model: InsightsModel,
422
+ input: InsightsEventPageInput,
423
+ filter: InsightsEventFilter,
424
+ map: (row: BundleEventRow) => T,
425
+ ) => {
426
+ const limit = readLimit(input.limit);
427
+ const cursor =
428
+ input.cursor === undefined
429
+ ? undefined
430
+ : readEventCursor(input.cursor, filter);
431
+ const bounds = readEventBounds(input, cursor, filter);
432
+ const query = {
433
+ filter,
434
+ ...bounds,
435
+ ...(cursor === undefined ? {} : { after: cursor.after }),
436
+ limit,
437
+ };
438
+ const rows = await model.listEvents(query);
439
+ assertEventRows(rows, query);
440
+ const last = rows.at(-1);
441
+ return {
442
+ beforeReceivedAtMs: bounds.beforeReceivedAtMs,
443
+ data: rows.map(map),
444
+ // A short page ends the range, so only a full page carries a cursor,
445
+ // with the range every page shares: the last call may read an empty
446
+ // page, and no page ends before the range start while events remain.
447
+ nextCursor:
448
+ rows.length === limit && last
449
+ ? encodeCursor({
450
+ after: { id: last.id, receivedAtMs: last.received_at_ms },
451
+ ...bounds,
452
+ kind: "events",
453
+ filter,
454
+ version: 2,
455
+ })
456
+ : null,
457
+ };
458
+ };
459
+
460
+ const assertInstallationRows = (
461
+ rows: readonly BundleEventRow[],
462
+ input: {
463
+ readonly afterInstallId?: string;
464
+ readonly limit: number;
465
+ readonly userId: string;
466
+ },
467
+ ): void => {
468
+ if (rows.length > input.limit) {
469
+ throw new Error("Insights database returned too many installation rows.");
470
+ }
471
+ for (let index = 0; index < rows.length; index += 1) {
472
+ const row = rows[index];
473
+ const previous = rows[index - 1];
474
+ if (
475
+ !row ||
476
+ row.user_id !== input.userId ||
477
+ (input.afterInstallId !== undefined &&
478
+ compareInsightsText(row.install_id, input.afterInstallId) <= 0) ||
479
+ (previous !== undefined &&
480
+ compareInsightsText(previous.install_id, row.install_id) >= 0)
481
+ ) {
482
+ throw new Error("Insights database returned invalid installation rows.");
483
+ }
484
+ }
485
+ };
486
+
487
+ export const createInsightsProvider = (
488
+ model: InsightsModel,
489
+ ): InsightsProvider =>
490
+ Object.freeze({
491
+ async appendBundleEvent(input) {
492
+ const event = createBundleEventRow(input);
493
+ await model.recordEvent({
494
+ event,
495
+ });
496
+ },
497
+ listEvents(input) {
498
+ const filter: InsightsEventFilter =
499
+ input.bundle === undefined
500
+ ? { kind: "all" }
501
+ : { kind: "bundle", ...bundleFilter(input.bundle) };
502
+ return pageEventRows(model, input, filter, toEventHistoryRow);
503
+ },
504
+ async listInstallationEvents(input: InsightsInstallationEventPageInput) {
505
+ if ("bundle" in input && input.bundle !== undefined) {
506
+ throw new InsightsBadRequestError(
507
+ "Installation movement queries cannot include a bundle filter.",
508
+ );
509
+ }
510
+ const installId = requireString(
511
+ input.installId,
512
+ "install ID",
513
+ MAX_IDENTITY_LENGTH,
514
+ );
515
+ return pageEventRows(
516
+ model,
517
+ input,
518
+ { kind: "installationMovement", installId },
519
+ (row) => toEventHistoryRow(row) as InstallationHistoryRow,
520
+ );
521
+ },
522
+ async getInstallation({ installId }) {
523
+ const normalizedInstallId = requireString(
524
+ installId,
525
+ "install ID",
526
+ MAX_IDENTITY_LENGTH,
527
+ );
528
+ const rows = await model.findLatestEvents({
529
+ installId: normalizedInstallId,
530
+ });
531
+ const row = rows[0] ?? null;
532
+ if (
533
+ rows.length > 1 ||
534
+ (row !== null && row.install_id !== normalizedInstallId)
535
+ ) {
536
+ throw new Error("Insights database returned an invalid installation.");
537
+ }
538
+ return row === null ? null : toInstallationRow(row);
539
+ },
540
+ async pageInstallationsByCurrentUserId(
541
+ input: InsightsUserInstallationPageInput,
542
+ ) {
543
+ const userId = requireString(
544
+ input.userId,
545
+ "user ID",
546
+ MAX_IDENTITY_LENGTH,
547
+ );
548
+ const limit = readLimit(input.limit);
549
+ const cursor =
550
+ input.cursor === undefined
551
+ ? undefined
552
+ : readUserInstallationCursor(input.cursor, userId);
553
+ const databaseInput = {
554
+ userId,
555
+ ...(cursor === undefined
556
+ ? {}
557
+ : { afterInstallId: cursor.afterInstallId }),
558
+ limit,
559
+ };
560
+ const rows = await model.findLatestEvents(databaseInput);
561
+ assertInstallationRows(rows, databaseInput);
562
+ const last = rows.at(-1);
563
+ return {
564
+ data: rows.map(toInstallationRow),
565
+ nextCursor:
566
+ rows.length === limit && last
567
+ ? encodeCursor({
568
+ afterInstallId: last.install_id,
569
+ kind: "user-installations",
570
+ userId,
571
+ version: 1,
572
+ })
573
+ : null,
574
+ };
575
+ },
576
+ async getReportingOverview(input) {
577
+ const scope = readScope(input);
578
+ const { window } = input;
579
+ if (!Object.hasOwn(WINDOW_MS, window)) {
580
+ throw new InsightsBadRequestError(
581
+ "Invalid reporting installation window.",
582
+ );
583
+ }
584
+ // Whole hours, ending with the current one: the counts read only the
585
+ // maintained hour and day rows, never raw events.
586
+ const now = Date.now();
587
+ const beforeReceivedAtMs =
588
+ now - (now % HOUR_MS) + (now % HOUR_MS === 0 ? 0 : HOUR_MS);
589
+ const sinceMs = Math.max(0, beforeReceivedAtMs - WINDOW_MS[window]);
590
+ const measure = async (count: Promise<number>) => {
591
+ const value = await count;
592
+ if (!Number.isSafeInteger(value) || value < 0) {
593
+ throw new Error("Insights database returned an invalid count.");
594
+ }
595
+ return { count: value, measuredAtMs: Date.now() };
596
+ };
597
+ const bundleId =
598
+ input.bundleId === undefined
599
+ ? undefined
600
+ : requireString(input.bundleId, "bundle ID", MAX_EVENT_ID_LENGTH);
601
+ const reporting = measure(model.countLatestEvents({ ...scope, sinceMs }));
602
+ if (bundleId === undefined) {
603
+ return {
604
+ ...scope,
605
+ window,
606
+ sinceMs,
607
+ beforeReceivedAtMs,
608
+ reportingInstallations: await reporting,
609
+ };
610
+ }
611
+ const countOutcome = (outcome: InsightsBundleSelection["outcome"]) =>
612
+ measure(
613
+ model.countEvents({
614
+ filter: bundleFilter({ ...scope, bundleId, outcome }),
615
+ sinceMs,
616
+ beforeReceivedAtMs,
617
+ }),
618
+ );
619
+ const [
620
+ reportingInstallations,
621
+ bundleInstallations,
622
+ downloadedReports,
623
+ appliedReports,
624
+ recoveredReports,
625
+ unchangedReports,
626
+ ] = await Promise.all([
627
+ reporting,
628
+ measure(
629
+ model.countLatestEvents({
630
+ ...scope,
631
+ sinceMs,
632
+ bundle: [
633
+ {
634
+ field: "from_bundle_id",
635
+ value: bundleId,
636
+ types: ["UPDATE_DOWNLOADED"],
637
+ },
638
+ {
639
+ field: "to_bundle_id",
640
+ value: bundleId,
641
+ types: ["UNCHANGED", "UPDATE_APPLIED", "RECOVERED"],
642
+ },
643
+ ],
644
+ }),
645
+ ),
646
+ countOutcome("downloaded"),
647
+ countOutcome("applied"),
648
+ countOutcome("recovered"),
649
+ countOutcome("unchanged"),
650
+ ]);
651
+ return {
652
+ ...scope,
653
+ window,
654
+ sinceMs,
655
+ beforeReceivedAtMs,
656
+ reportingInstallations,
657
+ bundle: {
658
+ bundleId,
659
+ reportingInstallations: bundleInstallations,
660
+ downloadedReports,
661
+ appliedReports,
662
+ recoveredReports,
663
+ unchangedReports,
664
+ },
665
+ };
666
+ },
667
+ });