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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/dist/_virtual/_rolldown/runtime.cjs +1 -1
  2. package/dist/adapters/databaseConstraintErrors.cjs +0 -1
  3. package/dist/adapters/databasePluginUtils.cjs +0 -1
  4. package/dist/adapters/drizzle.cjs +10 -3
  5. package/dist/adapters/drizzle.d.cts +0 -1
  6. package/dist/adapters/drizzle.d.mts +0 -1
  7. package/dist/adapters/drizzle.mjs +11 -3
  8. package/dist/adapters/drizzleCrud.cjs +62 -1
  9. package/dist/adapters/drizzleCrud.mjs +61 -2
  10. package/dist/adapters/drizzleLazyDB.cjs +6 -1
  11. package/dist/adapters/drizzleLazyDB.mjs +6 -1
  12. package/dist/adapters/drizzleQuery.cjs +3 -2
  13. package/dist/adapters/drizzleQuery.mjs +3 -1
  14. package/dist/adapters/insightsTransaction.cjs +22 -0
  15. package/dist/adapters/insightsTransaction.mjs +22 -0
  16. package/dist/adapters/kysely.cjs +2 -1
  17. package/dist/adapters/kysely.d.cts +0 -1
  18. package/dist/adapters/kysely.d.mts +0 -1
  19. package/dist/adapters/kysely.mjs +3 -1
  20. package/dist/adapters/kyselyCrud.cjs +54 -3
  21. package/dist/adapters/kyselyCrud.mjs +54 -3
  22. package/dist/adapters/kyselyQuery.cjs +2 -2
  23. package/dist/adapters/kyselyQuery.mjs +2 -1
  24. package/dist/adapters/mongodb.cjs +27 -1
  25. package/dist/adapters/mongodb.d.cts +1 -1
  26. package/dist/adapters/mongodb.d.mts +1 -1
  27. package/dist/adapters/mongodb.mjs +28 -1
  28. package/dist/adapters/mongodbCollections.cjs +2 -0
  29. package/dist/adapters/mongodbCollections.mjs +2 -0
  30. package/dist/adapters/mongodbQuery.cjs +21 -4
  31. package/dist/adapters/mongodbQuery.mjs +21 -5
  32. package/dist/adapters/mongodbReads.cjs +91 -29
  33. package/dist/adapters/mongodbReads.mjs +92 -29
  34. package/dist/adapters/mongodbWrites.cjs +16 -2
  35. package/dist/adapters/mongodbWrites.mjs +18 -3
  36. package/dist/adapters/prisma.cjs +55 -7
  37. package/dist/adapters/prisma.d.cts +0 -1
  38. package/dist/adapters/prisma.d.mts +0 -1
  39. package/dist/adapters/prisma.mjs +56 -7
  40. package/dist/adapters/prismaQuery.cjs +0 -1
  41. package/dist/adapters/prismaRows.cjs +22 -1
  42. package/dist/adapters/prismaRows.mjs +22 -1
  43. package/dist/apiKeys.cjs +6 -2
  44. package/dist/apiKeys.d.cts +0 -1
  45. package/dist/apiKeys.d.mts +0 -1
  46. package/dist/apiKeys.mjs +3 -3
  47. package/dist/createHotUpdaterCore.cjs +24 -9
  48. package/dist/createHotUpdaterCore.d.cts +1 -1
  49. package/dist/createHotUpdaterCore.d.mts +1 -1
  50. package/dist/createHotUpdaterCore.mjs +23 -9
  51. package/dist/db/createBundleDiff.cjs +2 -2
  52. package/dist/db/createBundleDiff.d.cts +1 -5
  53. package/dist/db/createBundleDiff.d.mts +1 -5
  54. package/dist/db/databasePluginCore.cjs +0 -1
  55. package/dist/db/fixedMigrator.cjs +2 -2
  56. package/dist/db/fixedMigrator.mjs +2 -2
  57. package/dist/db/fixedMigratorMongo.cjs +6 -4
  58. package/dist/db/fixedMigratorMongo.mjs +6 -4
  59. package/dist/db/index.d.cts +0 -1
  60. package/dist/db/index.d.mts +0 -1
  61. package/dist/db/releaseCatalog.cjs +0 -1
  62. package/dist/db/releaseCatalog.d.cts +0 -1
  63. package/dist/db/releaseCatalog.d.mts +0 -1
  64. package/dist/db/schema/insightsCollation.cjs +14 -0
  65. package/dist/db/schema/insightsCollation.mjs +14 -0
  66. package/dist/db/schema/sql.cjs +11 -7
  67. package/dist/db/schema/sql.mjs +8 -8
  68. package/dist/db/schemaGenerators.cjs +28 -9
  69. package/dist/db/schemaGenerators.mjs +28 -9
  70. package/dist/db/schemaReadiness.cjs +2 -0
  71. package/dist/db/schemaReadiness.d.cts +1 -0
  72. package/dist/db/schemaReadiness.d.mts +1 -0
  73. package/dist/db/schemaReadiness.mjs +2 -0
  74. package/dist/db/sqlMigrationExecution.cjs +1 -1
  75. package/dist/db/sqlMigrationExecution.mjs +1 -0
  76. package/dist/db/types.cjs +3 -1
  77. package/dist/db/types.d.cts +3 -6
  78. package/dist/db/types.d.mts +3 -6
  79. package/dist/db/types.mjs +2 -2
  80. package/dist/db/updateArtifacts.cjs +0 -1
  81. package/dist/handler.d.cts +1 -2
  82. package/dist/handler.d.mts +1 -2
  83. package/dist/handlerReleaseCatalogRoutes.cjs +0 -1
  84. package/dist/handlerReleaseManagementRoutes.cjs +0 -1
  85. package/dist/handlerTypes.d.cts +0 -1
  86. package/dist/handlerTypes.d.mts +0 -1
  87. package/dist/handlerVersionRoutes.d.cts +1 -0
  88. package/dist/handlerVersionRoutes.d.mts +1 -0
  89. package/dist/index.cjs +1 -1
  90. package/dist/index.d.cts +4 -4
  91. package/dist/index.d.mts +4 -4
  92. package/dist/index.mjs +1 -1
  93. package/dist/insights/domain.d.cts +37 -69
  94. package/dist/insights/domain.d.mts +37 -69
  95. package/dist/insights/errors.cjs +1 -8
  96. package/dist/insights/errors.mjs +2 -8
  97. package/dist/insights/eventInput.cjs +49 -5
  98. package/dist/insights/eventInput.mjs +48 -6
  99. package/dist/insights/provider.cjs +314 -0
  100. package/dist/insights/provider.d.cts +6 -0
  101. package/dist/insights/provider.d.mts +6 -0
  102. package/dist/insights/provider.mjs +314 -0
  103. package/dist/insights/queryInput.cjs +78 -43
  104. package/dist/insights/queryInput.mjs +76 -40
  105. package/dist/insights/routes.cjs +20 -25
  106. package/dist/insights/routes.mjs +22 -27
  107. package/dist/insights/types.d.cts +27 -12
  108. package/dist/insights/types.d.mts +27 -12
  109. package/dist/node.d.cts +0 -1
  110. package/dist/node.d.mts +0 -1
  111. package/dist/package.cjs +1 -1
  112. package/dist/package.mjs +1 -1
  113. package/dist/schema/index.cjs +17 -3
  114. package/dist/schema/index.mjs +4 -4
  115. package/dist/schema/v1_0_0.cjs +72 -30
  116. package/dist/schema/v1_0_0.mjs +72 -31
  117. package/dist/storageAccess.cjs +0 -1
  118. package/dist/types/index.d.cts +0 -1
  119. package/dist/types/index.d.mts +0 -1
  120. package/package.json +7 -7
  121. package/src/adapters/databasePluginTestDatabase.ts +14 -0
  122. package/src/adapters/drizzle.spec.ts +99 -0
  123. package/src/adapters/drizzle.ts +21 -3
  124. package/src/adapters/drizzleCrud.ts +154 -2
  125. package/src/adapters/drizzleLazyDB.ts +17 -3
  126. package/src/adapters/insightsTransaction.ts +32 -0
  127. package/src/adapters/kysely.spec.ts +29 -0
  128. package/src/adapters/kysely.ts +10 -1
  129. package/src/adapters/kyselyCrud.ts +121 -0
  130. package/src/adapters/mongodb.integration.spec.ts +210 -0
  131. package/src/adapters/mongodb.spec.ts +69 -1
  132. package/src/adapters/mongodb.ts +66 -2
  133. package/src/adapters/mongodbCollections.ts +5 -0
  134. package/src/adapters/mongodbQuery.ts +32 -3
  135. package/src/adapters/mongodbReads.ts +69 -0
  136. package/src/adapters/mongodbTestClient.ts +41 -6
  137. package/src/adapters/mongodbTestFilter.ts +4 -1
  138. package/src/adapters/mongodbWrites.ts +27 -0
  139. package/src/adapters/prisma.spec.ts +97 -0
  140. package/src/adapters/prisma.ts +103 -3
  141. package/src/adapters/prismaRows.ts +37 -1
  142. package/src/adapters/prismaTestClient.ts +22 -5
  143. package/src/apiKeys.integration.spec.ts +1 -1
  144. package/src/createHotUpdaterCore.ts +21 -7
  145. package/src/db/databasePluginUpdateCheck.bench.ts +3 -0
  146. package/src/db/fixedMigrator.spec.ts +65 -0
  147. package/src/db/fixedMigratorMongo.spec.ts +56 -9
  148. package/src/db/fixedMigratorMongo.ts +10 -10
  149. package/src/db/index.spec.ts +8 -1
  150. package/src/db/schema/insightsCollation.ts +21 -0
  151. package/src/db/schema/sql.ts +8 -5
  152. package/src/db/schemaGenerators.ts +41 -13
  153. package/src/db/types.spec.ts +8 -0
  154. package/src/db/types.ts +10 -4
  155. package/src/handler-standalone.integration.spec.ts +5 -7
  156. package/src/index.ts +2 -2
  157. package/src/insights/domain.ts +39 -75
  158. package/src/insights/errors.ts +0 -7
  159. package/src/insights/eventInput.ts +60 -3
  160. package/src/insights/insights.integration.spec.ts +254 -17
  161. package/src/insights/provider.spec.ts +385 -0
  162. package/src/insights/provider.ts +591 -0
  163. package/src/insights/queryInput.ts +112 -73
  164. package/src/insights/routes.ts +37 -56
  165. package/src/insights/types.ts +47 -36
  166. package/src/schema/dsl-all-versions.spec.ts +3 -1
  167. package/src/schema/v1_0_0.spec.ts +77 -3
  168. package/src/schema/v1_0_0.ts +94 -30
  169. package/dist/insights/bounded/activeOverview.cjs +0 -87
  170. package/dist/insights/bounded/activeOverview.mjs +0 -85
  171. package/dist/insights/bounded/installationSearch.cjs +0 -42
  172. package/dist/insights/bounded/installationSearch.mjs +0 -42
  173. package/dist/insights/bounded/persistence.cjs +0 -39
  174. package/dist/insights/bounded/persistence.mjs +0 -38
  175. package/dist/insights/bounded/provider.cjs +0 -170
  176. package/dist/insights/bounded/provider.d.cts +0 -7
  177. package/dist/insights/bounded/provider.d.mts +0 -7
  178. package/dist/insights/bounded/provider.mjs +0 -170
  179. package/dist/insights/bounded/scan.cjs +0 -144
  180. package/dist/insights/bounded/scan.mjs +0 -138
  181. package/dist/insights/persistence.d.cts +0 -6
  182. package/dist/insights/persistence.d.mts +0 -6
  183. package/src/insights/bounded/activeOverview.spec.ts +0 -189
  184. package/src/insights/bounded/activeOverview.ts +0 -148
  185. package/src/insights/bounded/installationSearch.spec.ts +0 -130
  186. package/src/insights/bounded/installationSearch.ts +0 -69
  187. package/src/insights/bounded/persistence.ts +0 -43
  188. package/src/insights/bounded/provider.spec.ts +0 -219
  189. package/src/insights/bounded/provider.ts +0 -229
  190. package/src/insights/bounded/scan.ts +0 -235
  191. package/src/insights/persistence.ts +0 -10
@@ -255,19 +255,30 @@ const releaseCatalogsV100 = require_dsl.table("release_catalogs", {
255
255
  referencedColumns: ["id"]
256
256
  })]
257
257
  });
258
+ const insightsTextCollations = {
259
+ postgresql: "\"C\"",
260
+ mysql: "utf8mb4_0900_bin"
261
+ };
262
+ const insightsProviders = [
263
+ "postgresql",
264
+ "mysql",
265
+ "sqlite",
266
+ "cockroachdb",
267
+ "mongodb"
268
+ ];
258
269
  const bundleEventsV100 = require_dsl.table("bundle_events", {
259
270
  id: require_dsl.idColumn("id", "uuid"),
260
- type: require_dsl.stringColumn("type"),
261
- install_id: require_dsl.stringColumn("install_id"),
262
- user_id: require_dsl.stringColumn("user_id").nullable(),
271
+ type: require_dsl.column("type", require_dsl.varchar(32)),
272
+ install_id: require_dsl.column("install_id", require_dsl.varchar(255)).collate(insightsTextCollations),
273
+ user_id: require_dsl.column("user_id", require_dsl.varchar(255)).collate(insightsTextCollations).nullable(),
263
274
  username: require_dsl.stringColumn("username").nullable(),
264
275
  from_release_id: require_dsl.uuid("from_release_id").nullable(),
265
276
  from_bundle_id: require_dsl.uuid("from_bundle_id").nullable(),
266
277
  to_release_id: require_dsl.uuid("to_release_id").nullable(),
267
278
  to_bundle_id: require_dsl.uuid("to_bundle_id"),
268
- platform: require_dsl.stringColumn("platform"),
279
+ platform: require_dsl.stringColumn("platform").collate(insightsTextCollations),
269
280
  app_version: require_dsl.stringColumn("app_version"),
270
- channel: require_dsl.stringColumn("channel"),
281
+ channel: require_dsl.stringColumn("channel").collate(insightsTextCollations),
271
282
  cohort: require_dsl.stringColumn("cohort"),
272
283
  update_strategy: require_dsl.stringColumn("update_strategy").nullable(),
273
284
  fingerprint_hash: require_dsl.stringColumn("fingerprint_hash").nullable(),
@@ -278,43 +289,26 @@ const bundleEventsV100 = require_dsl.table("bundle_events", {
278
289
  require_dsl.index("bundle_events_received_at_idx", ["received_at_ms", "id"]),
279
290
  require_dsl.index("bundle_events_install_idx", [
280
291
  "install_id",
281
- "received_at_ms",
282
- "id"
283
- ]),
284
- require_dsl.index("bundle_events_user_id_idx", [
285
- "user_id",
286
- "received_at_ms",
287
- "id"
288
- ]),
289
- require_dsl.index("bundle_events_username_idx", [
290
- "username",
291
- "received_at_ms",
292
- "id"
293
- ]),
294
- require_dsl.index("bundle_events_to_bundle_idx", [
295
292
  "type",
296
- "to_bundle_id",
297
293
  "received_at_ms",
298
294
  "id"
299
295
  ]),
300
296
  require_dsl.index("bundle_events_from_bundle_idx", [
301
297
  "type",
298
+ "platform",
299
+ "channel",
302
300
  "from_bundle_id",
303
301
  "received_at_ms",
304
302
  "id"
305
- ]),
306
- require_dsl.index("bundle_events_to_release_idx", [
307
- "type",
308
- "to_release_id",
309
- "received_at_ms",
310
- "id"
311
- ]),
312
- require_dsl.index("bundle_events_from_release_idx", [
303
+ ], insightsProviders),
304
+ require_dsl.index("bundle_events_to_bundle_idx", [
313
305
  "type",
314
- "from_release_id",
306
+ "platform",
307
+ "channel",
308
+ "to_bundle_id",
315
309
  "received_at_ms",
316
310
  "id"
317
- ])
311
+ ], insightsProviders)
318
312
  ],
319
313
  checks: [
320
314
  require_dsl.check({
@@ -339,6 +333,52 @@ const bundleEventsV100 = require_dsl.table("bundle_events", {
339
333
  })
340
334
  ]
341
335
  });
336
+ const bundleInstallationsV100 = require_dsl.table("bundle_installations", {
337
+ install_id: require_dsl.idColumn("install_id", require_dsl.varchar(255)).collate(insightsTextCollations),
338
+ id: require_dsl.uuid("id"),
339
+ user_id: require_dsl.column("user_id", require_dsl.varchar(255)).collate(insightsTextCollations).nullable(),
340
+ username: require_dsl.stringColumn("username").nullable(),
341
+ to_bundle_id: require_dsl.uuid("to_bundle_id"),
342
+ type: require_dsl.column("type", require_dsl.varchar(32)),
343
+ platform: require_dsl.stringColumn("platform").collate(insightsTextCollations),
344
+ app_version: require_dsl.stringColumn("app_version"),
345
+ channel: require_dsl.stringColumn("channel").collate(insightsTextCollations),
346
+ cohort: require_dsl.stringColumn("cohort"),
347
+ received_at_ms: require_dsl.float("received_at_ms")
348
+ }, {
349
+ indexes: [
350
+ require_dsl.index("bundle_installations_user_id_idx", ["user_id", "install_id"]),
351
+ require_dsl.index("bundle_installations_received_at_idx", ["received_at_ms"]),
352
+ require_dsl.index("bundle_installations_scope_idx", [
353
+ "platform",
354
+ "channel",
355
+ "received_at_ms"
356
+ ], insightsProviders),
357
+ require_dsl.index("bundle_installations_bundle_idx", [
358
+ "platform",
359
+ "channel",
360
+ "to_bundle_id",
361
+ "received_at_ms"
362
+ ], insightsProviders)
363
+ ],
364
+ checks: [
365
+ require_dsl.check({
366
+ name: "bundle_installations_type_check",
367
+ expression: "type in ('UPDATE_APPLIED', 'RECOVERED', 'RELEASE_ADOPTED', 'UNCHANGED')",
368
+ sqliteInline: true
369
+ }),
370
+ require_dsl.check({
371
+ name: "bundle_installations_platform_check",
372
+ expression: "platform in ('ios', 'android')",
373
+ sqliteInline: true
374
+ }),
375
+ require_dsl.check({
376
+ name: "bundle_installations_received_at_check",
377
+ expression: "received_at_ms >= 0",
378
+ sqliteInline: true
379
+ })
380
+ ]
381
+ });
342
382
  const v1_0_0 = require_dsl.schema({
343
383
  version: "1.0.0",
344
384
  settingsTable: require_types.HOT_UPDATER_SETTINGS_TABLE,
@@ -349,6 +389,7 @@ const v1_0_0 = require_dsl.schema({
349
389
  releasesV100,
350
390
  releaseCatalogsV100,
351
391
  bundleEventsV100,
392
+ bundleInstallationsV100,
352
393
  apiKeysV100,
353
394
  require_settings.createSettingsTable("1.0.0")
354
395
  ]
@@ -356,6 +397,7 @@ const v1_0_0 = require_dsl.schema({
356
397
  //#endregion
357
398
  exports.apiKeysV100 = apiKeysV100;
358
399
  exports.bundleEventsV100 = bundleEventsV100;
400
+ exports.bundleInstallationsV100 = bundleInstallationsV100;
359
401
  exports.bundlePatchesV100 = bundlePatchesV100;
360
402
  exports.bundlesV100 = bundlesV100;
361
403
  exports.channelsV100 = channelsV100;
@@ -255,19 +255,30 @@ const releaseCatalogsV100 = table("release_catalogs", {
255
255
  referencedColumns: ["id"]
256
256
  })]
257
257
  });
258
+ const insightsTextCollations = {
259
+ postgresql: "\"C\"",
260
+ mysql: "utf8mb4_0900_bin"
261
+ };
262
+ const insightsProviders = [
263
+ "postgresql",
264
+ "mysql",
265
+ "sqlite",
266
+ "cockroachdb",
267
+ "mongodb"
268
+ ];
258
269
  const bundleEventsV100 = table("bundle_events", {
259
270
  id: idColumn("id", "uuid"),
260
- type: stringColumn("type"),
261
- install_id: stringColumn("install_id"),
262
- user_id: stringColumn("user_id").nullable(),
271
+ type: column("type", varchar(32)),
272
+ install_id: column("install_id", varchar(255)).collate(insightsTextCollations),
273
+ user_id: column("user_id", varchar(255)).collate(insightsTextCollations).nullable(),
263
274
  username: stringColumn("username").nullable(),
264
275
  from_release_id: uuid("from_release_id").nullable(),
265
276
  from_bundle_id: uuid("from_bundle_id").nullable(),
266
277
  to_release_id: uuid("to_release_id").nullable(),
267
278
  to_bundle_id: uuid("to_bundle_id"),
268
- platform: stringColumn("platform"),
279
+ platform: stringColumn("platform").collate(insightsTextCollations),
269
280
  app_version: stringColumn("app_version"),
270
- channel: stringColumn("channel"),
281
+ channel: stringColumn("channel").collate(insightsTextCollations),
271
282
  cohort: stringColumn("cohort"),
272
283
  update_strategy: stringColumn("update_strategy").nullable(),
273
284
  fingerprint_hash: stringColumn("fingerprint_hash").nullable(),
@@ -278,43 +289,26 @@ const bundleEventsV100 = table("bundle_events", {
278
289
  index("bundle_events_received_at_idx", ["received_at_ms", "id"]),
279
290
  index("bundle_events_install_idx", [
280
291
  "install_id",
281
- "received_at_ms",
282
- "id"
283
- ]),
284
- index("bundle_events_user_id_idx", [
285
- "user_id",
286
- "received_at_ms",
287
- "id"
288
- ]),
289
- index("bundle_events_username_idx", [
290
- "username",
291
- "received_at_ms",
292
- "id"
293
- ]),
294
- index("bundle_events_to_bundle_idx", [
295
292
  "type",
296
- "to_bundle_id",
297
293
  "received_at_ms",
298
294
  "id"
299
295
  ]),
300
296
  index("bundle_events_from_bundle_idx", [
301
297
  "type",
298
+ "platform",
299
+ "channel",
302
300
  "from_bundle_id",
303
301
  "received_at_ms",
304
302
  "id"
305
- ]),
306
- index("bundle_events_to_release_idx", [
307
- "type",
308
- "to_release_id",
309
- "received_at_ms",
310
- "id"
311
- ]),
312
- index("bundle_events_from_release_idx", [
303
+ ], insightsProviders),
304
+ index("bundle_events_to_bundle_idx", [
313
305
  "type",
314
- "from_release_id",
306
+ "platform",
307
+ "channel",
308
+ "to_bundle_id",
315
309
  "received_at_ms",
316
310
  "id"
317
- ])
311
+ ], insightsProviders)
318
312
  ],
319
313
  checks: [
320
314
  check({
@@ -339,6 +333,52 @@ const bundleEventsV100 = table("bundle_events", {
339
333
  })
340
334
  ]
341
335
  });
336
+ const bundleInstallationsV100 = table("bundle_installations", {
337
+ install_id: idColumn("install_id", varchar(255)).collate(insightsTextCollations),
338
+ id: uuid("id"),
339
+ user_id: column("user_id", varchar(255)).collate(insightsTextCollations).nullable(),
340
+ username: stringColumn("username").nullable(),
341
+ to_bundle_id: uuid("to_bundle_id"),
342
+ type: column("type", varchar(32)),
343
+ platform: stringColumn("platform").collate(insightsTextCollations),
344
+ app_version: stringColumn("app_version"),
345
+ channel: stringColumn("channel").collate(insightsTextCollations),
346
+ cohort: stringColumn("cohort"),
347
+ received_at_ms: float("received_at_ms")
348
+ }, {
349
+ indexes: [
350
+ index("bundle_installations_user_id_idx", ["user_id", "install_id"]),
351
+ index("bundle_installations_received_at_idx", ["received_at_ms"]),
352
+ index("bundle_installations_scope_idx", [
353
+ "platform",
354
+ "channel",
355
+ "received_at_ms"
356
+ ], insightsProviders),
357
+ index("bundle_installations_bundle_idx", [
358
+ "platform",
359
+ "channel",
360
+ "to_bundle_id",
361
+ "received_at_ms"
362
+ ], insightsProviders)
363
+ ],
364
+ checks: [
365
+ check({
366
+ name: "bundle_installations_type_check",
367
+ expression: "type in ('UPDATE_APPLIED', 'RECOVERED', 'RELEASE_ADOPTED', 'UNCHANGED')",
368
+ sqliteInline: true
369
+ }),
370
+ check({
371
+ name: "bundle_installations_platform_check",
372
+ expression: "platform in ('ios', 'android')",
373
+ sqliteInline: true
374
+ }),
375
+ check({
376
+ name: "bundle_installations_received_at_check",
377
+ expression: "received_at_ms >= 0",
378
+ sqliteInline: true
379
+ })
380
+ ]
381
+ });
342
382
  const v1_0_0 = schema({
343
383
  version: "1.0.0",
344
384
  settingsTable: HOT_UPDATER_SETTINGS_TABLE,
@@ -349,9 +389,10 @@ const v1_0_0 = schema({
349
389
  releasesV100,
350
390
  releaseCatalogsV100,
351
391
  bundleEventsV100,
392
+ bundleInstallationsV100,
352
393
  apiKeysV100,
353
394
  createSettingsTable("1.0.0")
354
395
  ]
355
396
  });
356
397
  //#endregion
357
- export { apiKeysV100, bundleEventsV100, bundlePatchesV100, bundlesV100, channelsV100, releaseCatalogsV100, releasesV100, v1_0_0 };
398
+ export { apiKeysV100, bundleEventsV100, bundleInstallationsV100, bundlePatchesV100, bundlesV100, channelsV100, releaseCatalogsV100, releasesV100, v1_0_0 };
@@ -1,4 +1,3 @@
1
- require("./_virtual/_rolldown/runtime.cjs");
2
1
  let _hot_updater_plugin_core = require("@hot-updater/plugin-core");
3
2
  //#region src/storageAccess.ts
4
3
  const assertRemoteUrl = (value) => {
@@ -1,7 +1,6 @@
1
1
  import { HotUpdaterAPI } from "../createHotUpdaterCore.cjs";
2
2
  import { Bundle, Bundle as Bundle$2 } from "@hot-updater/core";
3
3
  import { ChannelRow } from "@hot-updater/plugin-core";
4
-
5
4
  //#region src/types/index.d.ts
6
5
  interface PaginationInfo {
7
6
  total: number;
@@ -1,7 +1,6 @@
1
1
  import { HotUpdaterAPI } from "../createHotUpdaterCore.mjs";
2
2
  import { ChannelRow } from "@hot-updater/plugin-core";
3
3
  import { Bundle as Bundle$1, Bundle as Bundle$2 } from "@hot-updater/core";
4
-
5
4
  //#region src/types/index.d.ts
6
5
  interface PaginationInfo {
7
6
  total: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hot-updater/server",
3
- "version": "1.0.0-rc.1",
3
+ "version": "1.0.0-rc.2",
4
4
  "type": "module",
5
5
  "description": "React Native OTA solution for self-hosted",
6
6
  "sideEffects": false,
@@ -54,10 +54,10 @@
54
54
  },
55
55
  "dependencies": {
56
56
  "rou3": "0.7.9",
57
- "verkit": "0.3.2",
58
- "@hot-updater/plugin-core": "1.0.0-rc.1",
59
- "@hot-updater/bsdiff": "1.0.0-rc.0",
60
- "@hot-updater/core": "1.0.0-rc.0"
57
+ "verkit": "0.4.0",
58
+ "@hot-updater/core": "1.0.0-rc.0",
59
+ "@hot-updater/plugin-core": "1.0.0-rc.2",
60
+ "@hot-updater/bsdiff": "1.0.0-rc.0"
61
61
  },
62
62
  "peerDependencies": {
63
63
  "@prisma/client": "*",
@@ -91,8 +91,8 @@
91
91
  "mongodb": "6.20.0",
92
92
  "msw": "^2.7.0",
93
93
  "uuidv7": "^1.0.2",
94
- "@hot-updater/standalone": "1.0.0-rc.1",
95
- "@hot-updater/test-utils": "1.0.0-rc.1"
94
+ "@hot-updater/standalone": "1.0.0-rc.2",
95
+ "@hot-updater/test-utils": "1.0.0-rc.2"
96
96
  },
97
97
  "scripts": {
98
98
  "build": "tsdown",
@@ -84,6 +84,19 @@ export const DATABASE_PLUGIN_TEST_SCHEMA_SQL = `
84
84
  sdk_version text,
85
85
  received_at_ms integer not null
86
86
  );
87
+ create table bundle_installations (
88
+ install_id varchar(255) primary key,
89
+ id text not null,
90
+ user_id text,
91
+ username text,
92
+ to_bundle_id text not null,
93
+ type text not null,
94
+ platform text not null,
95
+ app_version text not null,
96
+ channel text not null,
97
+ cohort text not null,
98
+ received_at_ms integer not null
99
+ );
87
100
  create table api_keys (
88
101
  id text primary key,
89
102
  hash text not null unique,
@@ -97,6 +110,7 @@ export const DATABASE_PLUGIN_TEST_SCHEMA_SQL = `
97
110
 
98
111
  export const DATABASE_PLUGIN_TEST_RESET_SQL = `
99
112
  delete from bundle_events;
113
+ delete from bundle_installations;
100
114
  delete from api_keys;
101
115
  delete from bundle_patches;
102
116
  delete from release_catalogs;
@@ -1,4 +1,5 @@
1
1
  import { PGlite } from "@electric-sql/pglite";
2
+ import { toInsightsInstallationRow } from "@hot-updater/plugin-core";
2
3
  import {
3
4
  boolean,
4
5
  doublePrecision,
@@ -11,6 +12,7 @@ import {
11
12
  import { drizzle } from "drizzle-orm/pglite";
12
13
  import { describe, expect, it, vi } from "vitest";
13
14
 
15
+ import { createBundleEventRowFixture } from "../../../test-utils/src/databaseTestFixtures";
14
16
  import { setupDatabasePluginTestSuite } from "../../../test-utils/src/setupDatabasePluginTestSuite";
15
17
  import type { DatabaseAdapterWithCapabilities } from "../db/types";
16
18
  import {
@@ -100,6 +102,19 @@ const bundleEvents = pgTable("bundle_events", {
100
102
  sdk_version: text("sdk_version"),
101
103
  received_at_ms: integer("received_at_ms").notNull(),
102
104
  });
105
+ const bundleInstallations = pgTable("bundle_installations", {
106
+ install_id: varchar("install_id", { length: 255 }).primaryKey(),
107
+ id: text("id").notNull(),
108
+ user_id: text("user_id"),
109
+ username: text("username"),
110
+ to_bundle_id: text("to_bundle_id").notNull(),
111
+ type: text("type").notNull(),
112
+ platform: text("platform").notNull(),
113
+ app_version: text("app_version").notNull(),
114
+ channel: text("channel").notNull(),
115
+ cohort: text("cohort").notNull(),
116
+ received_at_ms: integer("received_at_ms").notNull(),
117
+ });
103
118
  const apiKeys = pgTable("api_keys", {
104
119
  id: text("id").primaryKey(),
105
120
  hash: text("hash").notNull().unique(),
@@ -111,6 +126,7 @@ const apiKeys = pgTable("api_keys", {
111
126
  });
112
127
  const schema = {
113
128
  bundle_events: bundleEvents,
129
+ bundle_installations: bundleInstallations,
114
130
  bundle_patches: bundlePatches,
115
131
  bundles,
116
132
  channels,
@@ -156,6 +172,87 @@ setupDatabasePluginTestSuite({
156
172
  });
157
173
 
158
174
  describe("drizzleAdapter schema requirements", () => {
175
+ it("keeps Bun SQLite event and snapshot writes inside its synchronous transaction", async () => {
176
+ const { stdout } = await promisify(execFile)(
177
+ new URL("../../../../node_modules/.bin/bun", import.meta.url).pathname,
178
+ [
179
+ "-e",
180
+ `
181
+ import { Database } from "bun:sqlite";
182
+ import { drizzle } from "drizzle-orm/bun-sqlite";
183
+ import { toInsightsInstallationRow } from "@hot-updater/plugin-core";
184
+ import { drizzleAdapter } from "./src/adapters/drizzle.ts";
185
+ import { createTableSql } from "./src/db/schema/sql.ts";
186
+ import { createBundleEventRowFixture } from "../test-utils/src/databaseTestFixtures.ts";
187
+ import * as schema from "../../examples-server/elysia-drizzle-libsql/hot-updater-schema.ts";
188
+ const db = new Database(":memory:");
189
+ db.exec(createTableSql("sqlite").join(";"));
190
+ db.exec("create trigger reject_snapshot before insert on bundle_installations begin select raise(abort, 'injected snapshot failure'); end;");
191
+ const plugin = drizzleAdapter({ db: drizzle(db, { schema }), provider: "sqlite" });
192
+ const event = createBundleEventRowFixture("708", 100);
193
+ const input = { event, installation: toInsightsInstallationRow(event) };
194
+ let rejected = false;
195
+ try { await plugin.models.insights.record(input); } catch { rejected = true; }
196
+ if (!rejected || db.query("select count(*) as total from bundle_events").get().total !== 0) throw new Error("transaction failed to roll back");
197
+ db.exec("drop trigger reject_snapshot");
198
+ await plugin.models.insights.record(input);
199
+ await plugin.models.insights.record(input);
200
+ if (db.query("select count(*) as total from bundle_events").get().total !== 1 || db.query("select count(*) as total from bundle_installations").get().total !== 1) throw new Error("retry did not commit exactly one report");
201
+ db.close();
202
+ console.log("atomic retry verified");
203
+ `,
204
+ ],
205
+ { cwd: new URL("../..", import.meta.url).pathname },
206
+ );
207
+ expect(stdout).toContain("atomic retry verified");
208
+ });
209
+ it("rolls back the event when the installation write fails and permits retry", async () => {
210
+ const db = new PGlite();
211
+ await db.exec(DATABASE_PLUGIN_TEST_SCHEMA_SQL);
212
+ await db.exec(
213
+ "alter table bundle_installations add constraint reject_snapshot check (install_id <> 'install-701')",
214
+ );
215
+ const plugin = drizzleAdapter({
216
+ db: drizzle(db, { schema }),
217
+ provider: "postgresql",
218
+ });
219
+ const event = createBundleEventRowFixture("701", 100);
220
+ const input = { event, installation: toInsightsInstallationRow(event) };
221
+ try {
222
+ await expect(plugin.models.insights.record(input)).rejects.toThrow();
223
+ expect((await db.query("select id from bundle_events")).rows).toEqual([]);
224
+ await db.exec(
225
+ "alter table bundle_installations drop constraint reject_snapshot",
226
+ );
227
+ await plugin.models.insights.record(input);
228
+ await expect(
229
+ plugin.models.insights.findInstallations({
230
+ installId: event.install_id,
231
+ }),
232
+ ).resolves.toEqual([input.installation]);
233
+ } finally {
234
+ await db.close();
235
+ }
236
+ });
237
+
238
+ it("requires transaction support before recording on a lazy database", async () => {
239
+ const getDB = vi.fn(() => {
240
+ throw new DrizzleTestStateError();
241
+ });
242
+ const plugin = drizzleAdapter({
243
+ db: getDB,
244
+ provider: "postgresql",
245
+ schema,
246
+ });
247
+ const event = createBundleEventRowFixture("702", 100);
248
+ await expect(
249
+ plugin.models.insights.record({
250
+ event,
251
+ installation: toInsightsInstallationRow(event),
252
+ }),
253
+ ).rejects.toThrow("transaction support");
254
+ expect(getDB).not.toHaveBeenCalled();
255
+ });
159
256
  it("does not resolve a lazy database while generating a schema", () => {
160
257
  const getDB = vi.fn(() => {
161
258
  throw new DrizzleTestStateError();
@@ -235,3 +332,5 @@ describe("drizzleAdapter schema requirements", () => {
235
332
  );
236
333
  });
237
334
  });
335
+ import { execFile } from "node:child_process";
336
+ import { promisify } from "node:util";
@@ -15,8 +15,9 @@ import type {
15
15
  ORMProvider,
16
16
  SchemaGenerator,
17
17
  } from "../db/types";
18
- import { createDrizzleCrud } from "./drizzleCrud";
18
+ import { createDrizzleCrud, recordDrizzleInsights } from "./drizzleCrud";
19
19
  import { createLazyDB } from "./drizzleLazyDB";
20
+ import { runInsightsTransaction } from "./insightsTransaction";
20
21
 
21
22
  export type DrizzleProvider = Exclude<
22
23
  ORMProvider,
@@ -38,6 +39,18 @@ const createImplementation = (
38
39
  const transaction = db.transaction?.bind(db);
39
40
  return {
40
41
  ...crud,
42
+ recordInsights: (input) => {
43
+ if (transaction === undefined) {
44
+ throw new Error(
45
+ "Drizzle Insights recording requires transaction support.",
46
+ );
47
+ }
48
+ return runInsightsTransaction(() =>
49
+ transaction((transactionDatabase) =>
50
+ recordDrizzleInsights(transactionDatabase, config.provider, input),
51
+ ),
52
+ );
53
+ },
41
54
  deleteChannel: (input) => {
42
55
  if (transaction === undefined) {
43
56
  throw new Error(
@@ -116,8 +129,13 @@ export const drizzleAdapter = (
116
129
  delete: (input) => getAdapter().models.channels.delete(input),
117
130
  },
118
131
  insights: {
119
- append: (row) => getAdapter().models.insights.append(row),
120
- scan: (input) => getAdapter().models.insights.scan(input),
132
+ record: (input) => getAdapter().models.insights.record(input),
133
+ listEvents: (input) => getAdapter().models.insights.listEvents(input),
134
+ findInstallations: (input) =>
135
+ getAdapter().models.insights.findInstallations(input),
136
+ countInstallations: (input) =>
137
+ getAdapter().models.insights.countInstallations(input),
138
+ countEvents: (input) => getAdapter().models.insights.countEvents(input),
121
139
  },
122
140
  apiKeys: {
123
141
  create: (row) => getAdapter().models.apiKeys.create(row),