@hot-updater/server 0.36.7 → 1.0.0-rc.0

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 (291) hide show
  1. package/dist/adapters/databaseConstraintErrors.cjs +23 -0
  2. package/dist/adapters/databaseConstraintErrors.mjs +21 -0
  3. package/dist/adapters/databasePluginUtils.cjs +117 -0
  4. package/dist/adapters/databasePluginUtils.mjs +106 -0
  5. package/dist/adapters/drizzle.cjs +53 -176
  6. package/dist/adapters/drizzle.d.cts +6 -4
  7. package/dist/adapters/drizzle.d.mts +6 -4
  8. package/dist/adapters/drizzle.mjs +54 -177
  9. package/dist/adapters/drizzleCrud.cjs +250 -0
  10. package/dist/adapters/drizzleCrud.mjs +249 -0
  11. package/dist/adapters/drizzleLazyDB.cjs +84 -36
  12. package/dist/adapters/drizzleLazyDB.mjs +84 -36
  13. package/dist/adapters/drizzleQuery.cjs +68 -0
  14. package/dist/adapters/drizzleQuery.mjs +67 -0
  15. package/dist/adapters/kysely.cjs +18 -138
  16. package/dist/adapters/kysely.d.cts +3 -12
  17. package/dist/adapters/kysely.d.mts +3 -12
  18. package/dist/adapters/kysely.mjs +19 -139
  19. package/dist/adapters/kyselyCrud.cjs +254 -0
  20. package/dist/adapters/kyselyCrud.mjs +253 -0
  21. package/dist/adapters/kyselyQuery.cjs +56 -0
  22. package/dist/adapters/kyselyQuery.mjs +55 -0
  23. package/dist/adapters/mongodb.cjs +21 -174
  24. package/dist/adapters/mongodb.d.cts +3 -2
  25. package/dist/adapters/mongodb.d.mts +3 -2
  26. package/dist/adapters/mongodb.mjs +21 -174
  27. package/dist/adapters/mongodbCollections.cjs +36 -0
  28. package/dist/adapters/mongodbCollections.mjs +30 -0
  29. package/dist/adapters/mongodbQuery.cjs +77 -0
  30. package/dist/adapters/mongodbQuery.mjs +70 -0
  31. package/dist/adapters/mongodbReads.cjs +148 -0
  32. package/dist/adapters/mongodbReads.mjs +147 -0
  33. package/dist/adapters/mongodbWrites.cjs +134 -0
  34. package/dist/adapters/mongodbWrites.mjs +133 -0
  35. package/dist/adapters/prisma.cjs +205 -180
  36. package/dist/adapters/prisma.d.cts +3 -3
  37. package/dist/adapters/prisma.d.mts +3 -3
  38. package/dist/adapters/prisma.mjs +206 -181
  39. package/dist/adapters/prismaQuery.cjs +58 -0
  40. package/dist/adapters/prismaQuery.mjs +56 -0
  41. package/dist/adapters/prismaRows.cjs +198 -0
  42. package/dist/adapters/prismaRows.mjs +188 -0
  43. package/dist/analytics/bounded/activeOverview.cjs +87 -0
  44. package/dist/analytics/bounded/activeOverview.mjs +85 -0
  45. package/dist/analytics/bounded/installationSearch.cjs +42 -0
  46. package/dist/analytics/bounded/installationSearch.mjs +42 -0
  47. package/dist/analytics/bounded/persistence.cjs +39 -0
  48. package/dist/analytics/bounded/persistence.mjs +38 -0
  49. package/dist/analytics/bounded/provider.cjs +170 -0
  50. package/dist/analytics/bounded/provider.d.cts +7 -0
  51. package/dist/analytics/bounded/provider.d.mts +7 -0
  52. package/dist/analytics/bounded/provider.mjs +170 -0
  53. package/dist/analytics/bounded/scan.cjs +144 -0
  54. package/dist/analytics/bounded/scan.mjs +138 -0
  55. package/dist/analytics/domain.d.cts +121 -0
  56. package/dist/analytics/domain.d.mts +121 -0
  57. package/dist/analytics/errors.cjs +22 -0
  58. package/dist/analytics/errors.mjs +20 -0
  59. package/dist/analytics/eventInput.cjs +111 -0
  60. package/dist/analytics/eventInput.mjs +111 -0
  61. package/dist/analytics/persistence.d.cts +6 -0
  62. package/dist/analytics/persistence.d.mts +6 -0
  63. package/dist/analytics/queryInput.cjs +58 -0
  64. package/dist/analytics/queryInput.mjs +55 -0
  65. package/dist/analytics/routes.cjs +63 -0
  66. package/dist/analytics/routes.mjs +61 -0
  67. package/dist/analytics/types.d.cts +17 -0
  68. package/dist/analytics/types.d.mts +17 -0
  69. package/dist/apiKeys.cjs +139 -0
  70. package/dist/apiKeys.d.cts +50 -0
  71. package/dist/apiKeys.d.mts +50 -0
  72. package/dist/apiKeys.mjs +133 -0
  73. package/dist/createHotUpdaterCore.cjs +48 -26
  74. package/dist/createHotUpdaterCore.d.cts +49 -17
  75. package/dist/createHotUpdaterCore.d.mts +49 -17
  76. package/dist/createHotUpdaterCore.mjs +51 -29
  77. package/dist/db/createBundleDiff.cjs +45 -68
  78. package/dist/db/createBundleDiff.d.cts +3 -3
  79. package/dist/db/createBundleDiff.d.mts +3 -3
  80. package/dist/db/createBundleDiff.mjs +47 -68
  81. package/dist/db/databasePluginCore.cjs +138 -0
  82. package/dist/db/databasePluginCore.mjs +137 -0
  83. package/dist/db/fixedMigrator.cjs +36 -93
  84. package/dist/db/fixedMigrator.mjs +36 -93
  85. package/dist/db/fixedMigratorMongo.cjs +319 -0
  86. package/dist/db/fixedMigratorMongo.mjs +319 -0
  87. package/dist/db/fixedMigratorShared.cjs +23 -0
  88. package/dist/db/fixedMigratorShared.mjs +18 -0
  89. package/dist/db/mongoMigrationExecution.cjs +11 -0
  90. package/dist/db/mongoMigrationExecution.mjs +11 -0
  91. package/dist/db/releaseCatalog.cjs +66 -0
  92. package/dist/db/releaseCatalog.d.cts +17 -0
  93. package/dist/db/releaseCatalog.d.mts +17 -0
  94. package/dist/db/releaseCatalog.mjs +64 -0
  95. package/dist/db/schema/mongodb.cjs +14 -6
  96. package/dist/db/schema/mongodb.mjs +14 -6
  97. package/dist/db/schema/registry.cjs +0 -2
  98. package/dist/db/schema/registry.mjs +1 -2
  99. package/dist/db/schema/sql.cjs +31 -14
  100. package/dist/db/schema/sql.mjs +32 -13
  101. package/dist/db/schema/sqlOperations.cjs +4 -4
  102. package/dist/db/schema/sqlOperations.mjs +5 -5
  103. package/dist/db/schemaGenerators.cjs +52 -18
  104. package/dist/db/schemaGenerators.mjs +52 -18
  105. package/dist/db/schemaReadiness.cjs +4 -3
  106. package/dist/db/schemaReadiness.mjs +4 -3
  107. package/dist/db/sqlMigrationExecution.cjs +67 -0
  108. package/dist/db/sqlMigrationExecution.mjs +66 -0
  109. package/dist/db/types.cjs +2 -6
  110. package/dist/db/types.d.cts +60 -16
  111. package/dist/db/types.d.mts +60 -16
  112. package/dist/db/types.mjs +3 -6
  113. package/dist/db/updateArtifacts.cjs +114 -60
  114. package/dist/db/updateArtifacts.mjs +117 -62
  115. package/dist/handler.cjs +108 -282
  116. package/dist/handler.d.cts +3 -46
  117. package/dist/handler.d.mts +3 -46
  118. package/dist/handler.mjs +107 -281
  119. package/dist/handlerBundleRoutes.cjs +155 -0
  120. package/dist/handlerBundleRoutes.mjs +155 -0
  121. package/dist/handlerErrors.cjs +9 -0
  122. package/dist/handlerErrors.mjs +9 -0
  123. package/dist/handlerParameters.cjs +39 -0
  124. package/dist/handlerParameters.mjs +34 -0
  125. package/dist/handlerReleaseCatalogRoutes.cjs +111 -0
  126. package/dist/handlerReleaseCatalogRoutes.mjs +110 -0
  127. package/dist/handlerReleaseManagementRoutes.cjs +125 -0
  128. package/dist/handlerReleaseManagementRoutes.mjs +124 -0
  129. package/dist/handlerTypes.d.cts +63 -0
  130. package/dist/handlerTypes.d.mts +63 -0
  131. package/dist/handlerVersionRoutes.cjs +10 -0
  132. package/dist/handlerVersionRoutes.d.cts +4 -0
  133. package/dist/handlerVersionRoutes.d.mts +4 -0
  134. package/dist/handlerVersionRoutes.mjs +9 -0
  135. package/dist/index.cjs +10 -1
  136. package/dist/index.d.cts +9 -3
  137. package/dist/index.d.mts +9 -3
  138. package/dist/index.mjs +5 -2
  139. package/dist/node.cjs +19 -12
  140. package/dist/node.d.cts +15 -10
  141. package/dist/node.d.mts +15 -10
  142. package/dist/node.mjs +19 -13
  143. package/dist/package.cjs +1 -1
  144. package/dist/package.mjs +1 -1
  145. package/dist/schema/dsl.cjs +19 -2
  146. package/dist/schema/dsl.mjs +17 -3
  147. package/dist/schema/index.cjs +1 -8
  148. package/dist/schema/index.mjs +2 -8
  149. package/dist/schema/types.cjs +3 -1
  150. package/dist/schema/types.mjs +3 -2
  151. package/dist/schema/v1_0_0.cjs +364 -0
  152. package/dist/schema/v1_0_0.mjs +357 -0
  153. package/dist/storageAccess.cjs +69 -25
  154. package/dist/storageAccess.mjs +68 -25
  155. package/dist/types/index.d.cts +4 -3
  156. package/dist/types/index.d.mts +5 -4
  157. package/package.json +8 -7
  158. package/src/adapters/databaseConstraintErrors.ts +25 -0
  159. package/src/adapters/databasePluginTestDatabase.ts +106 -0
  160. package/src/adapters/databasePluginUtils.spec.ts +29 -0
  161. package/src/adapters/databasePluginUtils.ts +214 -0
  162. package/src/adapters/drizzle.spec.ts +208 -47
  163. package/src/adapters/drizzle.ts +106 -293
  164. package/src/adapters/drizzleCrud.ts +419 -0
  165. package/src/adapters/drizzleLazyDB.ts +206 -100
  166. package/src/adapters/drizzleQuery.ts +164 -0
  167. package/src/adapters/kysely.spec.ts +126 -88
  168. package/src/adapters/kysely.ts +76 -334
  169. package/src/adapters/kyselyCrud.ts +588 -0
  170. package/src/adapters/kyselyQuery.ts +128 -0
  171. package/src/adapters/mongodb.spec.ts +141 -0
  172. package/src/adapters/mongodb.ts +60 -288
  173. package/src/adapters/mongodbCollections.ts +62 -0
  174. package/src/adapters/mongodbQuery.ts +226 -0
  175. package/src/adapters/mongodbReads.ts +311 -0
  176. package/src/adapters/mongodbRows.spec.ts +64 -0
  177. package/src/adapters/mongodbRows.ts +118 -0
  178. package/src/adapters/mongodbTestClient.ts +342 -0
  179. package/src/adapters/mongodbTestFilter.ts +166 -0
  180. package/src/adapters/mongodbWrites.ts +260 -0
  181. package/src/adapters/prisma.spec.ts +196 -0
  182. package/src/adapters/prisma.ts +417 -292
  183. package/src/adapters/prismaQuery.ts +129 -0
  184. package/src/adapters/prismaRows.spec.ts +98 -0
  185. package/src/adapters/prismaRows.ts +301 -0
  186. package/src/adapters/prismaTestClient.ts +396 -0
  187. package/src/analytics/analytics.integration.spec.ts +144 -0
  188. package/src/analytics/bounded/activeOverview.spec.ts +189 -0
  189. package/src/analytics/bounded/activeOverview.ts +148 -0
  190. package/src/analytics/bounded/installationSearch.spec.ts +130 -0
  191. package/src/analytics/bounded/installationSearch.ts +69 -0
  192. package/src/analytics/bounded/persistence.ts +43 -0
  193. package/src/analytics/bounded/provider.spec.ts +219 -0
  194. package/src/analytics/bounded/provider.ts +229 -0
  195. package/src/analytics/bounded/scan.ts +235 -0
  196. package/src/analytics/domain.ts +139 -0
  197. package/src/analytics/errors.ts +18 -0
  198. package/src/analytics/eventInput.ts +168 -0
  199. package/src/analytics/persistence.ts +10 -0
  200. package/src/analytics/queryInput.ts +102 -0
  201. package/src/analytics/routes.ts +123 -0
  202. package/src/analytics/types.ts +44 -0
  203. package/src/apiKeys.integration.spec.ts +192 -0
  204. package/src/apiKeys.spec.ts +162 -0
  205. package/src/apiKeys.ts +220 -0
  206. package/src/createHotUpdaterCore.ts +164 -100
  207. package/src/db/bundleRows.spec.ts +164 -0
  208. package/src/db/bundleRows.ts +10 -140
  209. package/src/db/createBundleDiff.spec.ts +172 -127
  210. package/src/db/createBundleDiff.ts +91 -110
  211. package/src/db/databasePluginCore.spec.ts +62 -0
  212. package/src/db/databasePluginCore.testFixtures.ts +76 -0
  213. package/src/db/databasePluginCore.ts +185 -0
  214. package/src/db/databasePluginUpdateCheck.bench.ts +107 -0
  215. package/src/db/fixedMigrator.spec.ts +184 -42
  216. package/src/db/fixedMigrator.ts +64 -205
  217. package/src/db/fixedMigratorMongo.spec.ts +192 -0
  218. package/src/db/fixedMigratorMongo.ts +425 -0
  219. package/src/db/fixedMigratorShared.ts +36 -0
  220. package/src/db/hotUpdaterSchema.ts +0 -1
  221. package/src/db/index.spec.ts +333 -1025
  222. package/src/db/mongoMigrationExecution.ts +23 -0
  223. package/src/db/node-sqlite.d.ts +25 -0
  224. package/src/db/releaseCatalog.ts +148 -0
  225. package/src/db/schema/mongodb.ts +20 -6
  226. package/src/db/schema/sql.ts +52 -11
  227. package/src/db/schema/sqlOperations.ts +5 -4
  228. package/src/db/schemaGenerators.ts +107 -23
  229. package/src/db/schemaReadiness.ts +2 -1
  230. package/src/db/sqlMigrationExecution.spec.ts +65 -0
  231. package/src/db/sqlMigrationExecution.ts +108 -0
  232. package/src/db/types.spec.ts +55 -0
  233. package/src/db/types.ts +129 -65
  234. package/src/db/updateArtifacts.spec.ts +359 -0
  235. package/src/db/updateArtifacts.ts +290 -126
  236. package/src/handler-standalone.integration.spec.ts +401 -364
  237. package/src/handler.spec.ts +137 -582
  238. package/src/handler.testFixtures.ts +30 -0
  239. package/src/handler.ts +218 -616
  240. package/src/handlerAuth.integration.spec.ts +33 -0
  241. package/src/handlerBundleRoutes.ts +259 -0
  242. package/src/handlerErrors.ts +7 -0
  243. package/src/handlerManagement.spec.ts +262 -0
  244. package/src/handlerParameters.ts +105 -0
  245. package/src/handlerReleaseCatalogRoutes.ts +154 -0
  246. package/src/handlerReleaseManagementRoutes.ts +213 -0
  247. package/src/handlerTypes.ts +96 -0
  248. package/src/handlerVersionRoutes.ts +12 -0
  249. package/src/index.ts +22 -2
  250. package/src/node.spec.ts +79 -10
  251. package/src/node.ts +20 -17
  252. package/src/releaseCatalogRoutes.integration.spec.ts +238 -0
  253. package/src/runtime.spec.ts +126 -959
  254. package/src/runtime.testFixtures.ts +71 -0
  255. package/src/schema/dsl-all-versions.spec.ts +26 -17
  256. package/src/schema/dsl.ts +41 -2
  257. package/src/schema/index.ts +3 -9
  258. package/src/schema/types.ts +9 -3
  259. package/src/schema/v1_0_0.spec.ts +146 -0
  260. package/src/schema/v1_0_0.ts +465 -0
  261. package/src/storageAccess.spec.ts +127 -32
  262. package/src/storageAccess.ts +106 -55
  263. package/src/types/index.ts +2 -1
  264. package/dist/db/bundleRows.cjs +0 -82
  265. package/dist/db/bundleRows.d.cts +0 -33
  266. package/dist/db/bundleRows.d.mts +0 -33
  267. package/dist/db/bundleRows.mjs +0 -79
  268. package/dist/db/pluginCore.cjs +0 -242
  269. package/dist/db/pluginCore.mjs +0 -241
  270. package/dist/db/schema/sqlMigrations.cjs +0 -74
  271. package/dist/db/schema/sqlMigrations.mjs +0 -73
  272. package/dist/db/schemaEnhancements.cjs +0 -20
  273. package/dist/db/schemaEnhancements.mjs +0 -19
  274. package/dist/route.cjs +0 -7
  275. package/dist/route.mjs +0 -7
  276. package/dist/schema/v0_21_0.cjs +0 -38
  277. package/dist/schema/v0_21_0.mjs +0 -37
  278. package/dist/schema/v0_29_0.cjs +0 -45
  279. package/dist/schema/v0_29_0.mjs +0 -44
  280. package/dist/schema/v0_31_0.cjs +0 -82
  281. package/dist/schema/v0_31_0.mjs +0 -80
  282. package/src/db/pluginCore.spec.ts +0 -1095
  283. package/src/db/pluginCore.ts +0 -475
  284. package/src/db/pluginUpdateCheck.bench.ts +0 -254
  285. package/src/db/schema/sqlMigrations.ts +0 -219
  286. package/src/db/schemaEnhancements.ts +0 -55
  287. package/src/route.ts +0 -7
  288. package/src/schema/v0_21_0.ts +0 -52
  289. package/src/schema/v0_29_0.ts +0 -62
  290. package/src/schema/v0_31_0.spec.ts +0 -73
  291. package/src/schema/v0_31_0.ts +0 -120
@@ -1,7 +1,15 @@
1
1
  import { PGlite } from "@electric-sql/pglite";
2
2
  import type { Bundle } from "@hot-updater/core";
3
- import { NIL_UUID } from "@hot-updater/core";
4
- import { createBlobDatabasePlugin } from "@hot-updater/plugin-core";
3
+ import {
4
+ createReleaseCatalogScopeKey,
5
+ encodeChannelKey,
6
+ NIL_UUID,
7
+ } from "@hot-updater/core";
8
+ import {
9
+ bundleToRow,
10
+ commitReleaseCatalogMutation,
11
+ createDatabaseClient,
12
+ } from "@hot-updater/plugin-core";
5
13
  import { Kysely } from "kysely";
6
14
  import { PGliteDialect } from "kysely-pglite-dialect";
7
15
  import { HttpResponse, http } from "msw";
@@ -10,90 +18,60 @@ import { uuidv7 } from "uuidv7";
10
18
  import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
11
19
 
12
20
  import { standaloneRepository } from "../../../plugins/standalone/src";
21
+ import { createInMemoryDatabasePlugin } from "../../test-utils/test/inMemoryDatabasePlugin";
13
22
  import { kyselyAdapter } from "./adapters/kysely";
14
23
  import { createMigrator } from "./db";
15
- import { createHotUpdater } from "./index";
16
-
17
- /**
18
- * Integration tests between @hot-updater/server handler and @hot-updater/standalone repository
19
- *
20
- * This test suite verifies real-world compatibility by:
21
- * 1. Using actual standaloneRepository (not mocks)
22
- * 2. Using actual handler (not mocks)
23
- * 3. Simulating HTTP communication via MSW
24
- * 4. Testing end-to-end flows: standalone → HTTP → handler → database
25
- */
26
-
27
- // Create in-memory database for testing
24
+ import { createHotUpdater, type RuntimeHotUpdaterAPI } from "./index";
25
+
28
26
  const db = new PGlite();
29
27
  const kysely = new Kysely<object>({ dialect: new PGliteDialect(db) });
30
-
31
- // Create handler API with in-memory DB
28
+ const clientMountPath = "/hot-updater";
32
29
  const api = createHotUpdater({
33
- database: kyselyAdapter({
34
- db: kysely,
35
- provider: "postgresql",
36
- }),
37
- basePath: "/hot-updater",
38
- routes: {
39
- updateCheck: true,
40
- bundles: true,
41
- },
30
+ database: kyselyAdapter({ db: kysely, provider: "postgresql" }),
31
+ clientAccess: { type: "public" },
42
32
  });
43
-
44
- // Setup MSW server to intercept HTTP requests
45
33
  const baseUrl = "http://localhost:3000";
46
34
  const server = setupServer();
35
+ const invokeHandler = async (
36
+ hotUpdater: RuntimeHotUpdaterAPI,
37
+ request: Request,
38
+ clientPath: string,
39
+ ) => {
40
+ const url = new URL(request.url);
41
+ const adminBasePath = `${clientPath}/admin`;
42
+ const isAdmin = url.pathname.startsWith(`${adminBasePath}/`);
43
+ const mountPath = isAdmin ? adminBasePath : clientPath;
44
+ url.pathname = url.pathname.slice(mountPath.length) || "/";
45
+ const handler = isAdmin
46
+ ? hotUpdater.handlers.admin
47
+ : hotUpdater.handlers.client;
48
+ const response = await handler(new Request(url, request));
49
+ return new HttpResponse(await response.text(), {
50
+ status: response.status,
51
+ headers: response.headers,
52
+ });
53
+ };
47
54
 
48
55
  beforeAll(async () => {
49
- // Initialize database
50
- const migrator = createMigrator(api);
51
- const result = await migrator.migrateToLatest({
56
+ const result = await createMigrator(api).migrateToLatest({
52
57
  mode: "from-schema",
53
58
  updateSettings: true,
54
59
  });
55
60
  await result.execute();
56
-
57
- // Start MSW server
58
61
  server.listen({ onUnhandledRequest: "error" });
59
-
60
- // Route all requests to our handler
61
- const handleRequest = async (request: Request) => {
62
- const response = await api.handler(request);
63
- const data = (await response.json()) as Record<string, unknown>;
64
- return HttpResponse.json(data, {
65
- status: response.status,
66
- headers: response.headers,
67
- });
68
- };
69
-
70
62
  server.use(
71
- // Specific routes
72
- http.get(`${baseUrl}/hot-updater/api/bundles`, ({ request }) =>
73
- handleRequest(request),
74
- ),
75
- http.get(`${baseUrl}/hot-updater/api/bundles/channels`, ({ request }) =>
76
- handleRequest(request),
77
- ),
78
- http.get(`${baseUrl}/hot-updater/api/bundles/:id`, ({ request }) =>
79
- handleRequest(request),
80
- ),
81
- http.post(`${baseUrl}/hot-updater/api/bundles`, ({ request }) =>
82
- handleRequest(request),
83
- ),
84
- http.patch(`${baseUrl}/hot-updater/api/bundles/:id`, ({ request }) =>
85
- handleRequest(request),
86
- ),
87
- http.delete(`${baseUrl}/hot-updater/api/bundles/:id`, ({ request }) =>
88
- handleRequest(request),
63
+ http.all(`${baseUrl}/hot-updater/*`, ({ request }) =>
64
+ invokeHandler(api, request, clientMountPath),
89
65
  ),
90
66
  );
91
67
  });
92
68
 
93
69
  afterEach(async () => {
94
- // Clean up database after each test
95
70
  await db.exec("DELETE FROM bundle_patches");
71
+ await db.exec("DELETE FROM release_catalogs");
72
+ await db.exec("DELETE FROM releases");
96
73
  await db.exec("DELETE FROM bundles");
74
+ await db.exec("DELETE FROM channels");
97
75
  });
98
76
 
99
77
  afterAll(async () => {
@@ -105,389 +83,448 @@ afterAll(async () => {
105
83
  const createTestBundle = (overrides?: Partial<Bundle>): Bundle => ({
106
84
  id: NIL_UUID,
107
85
  platform: "ios",
108
- channel: "production",
109
- enabled: true,
110
- shouldForceUpdate: false,
111
86
  fileHash: "test-hash",
112
87
  gitCommitHash: null,
113
- message: null,
114
- targetAppVersion: "*",
115
88
  storageUri: "test://storage",
116
- fingerprintHash: null,
89
+ archiveByteSize: 3_000_000_001,
117
90
  ...overrides,
118
91
  });
119
92
 
120
- const createInMemoryBlobDatabase = (store: Record<string, string>) =>
121
- createBlobDatabasePlugin({
122
- name: "blob-test",
123
- factory: () => ({
124
- apiBasePath: "/api/check-update",
125
- listObjects: async (prefix: string) =>
126
- Object.keys(store).filter((key) => key.startsWith(prefix)),
127
- loadObject: async <T>(key: string) => {
128
- const value = store[key];
129
- return value ? (JSON.parse(value) as T) : null;
130
- },
131
- uploadObject: async <T>(key: string, data: T) => {
132
- store[key] = JSON.stringify(data);
93
+ const createStandaloneClient = (base = `${baseUrl}/hot-updater/admin`) =>
94
+ createDatabaseClient(standaloneRepository({ baseUrl: base }));
95
+
96
+ const commitTestRelease = async (
97
+ bundle: Bundle,
98
+ channelName = "production",
99
+ ) => {
100
+ const repository = standaloneRepository({
101
+ baseUrl: `${baseUrl}/hot-updater/admin`,
102
+ });
103
+ const channel = await repository.models.channels.insert({
104
+ onConflict: "returnExisting",
105
+ row: { id: uuidv7(), name: channelName },
106
+ });
107
+ const scopeKey = createReleaseCatalogScopeKey({
108
+ channelKey: encodeChannelKey(channelName),
109
+ platform: bundle.platform,
110
+ strategy: "APP_VERSION",
111
+ });
112
+ const releaseId = uuidv7();
113
+ const committed = await commitReleaseCatalogMutation({
114
+ companionChanges: [
115
+ {
116
+ model: "bundles",
117
+ operation: "insert",
118
+ row: bundleToRow(bundle),
133
119
  },
134
- deleteObject: async (key: string) => {
135
- delete store[key];
120
+ ],
121
+ database: repository,
122
+ mutation: {
123
+ operation: "insert",
124
+ row: {
125
+ bundle_id: bundle.id,
126
+ channel_id: channel.row.id,
127
+ created_at_ms: 1,
128
+ enabled: true,
129
+ fingerprint_hash: null,
130
+ id: releaseId,
131
+ kind: "BUNDLE",
132
+ message: null,
133
+ operation: "DEPLOY",
134
+ platform: bundle.platform,
135
+ revision: 1,
136
+ rollout_cohort_count: 1_000,
137
+ scope_key: scopeKey,
138
+ should_force_update: false,
139
+ source_release_id: null,
140
+ strategy: "APP_VERSION",
141
+ target_app_version: "*",
142
+ target_cohorts: [],
143
+ updated_at_ms: 1,
136
144
  },
137
- invalidatePaths: async () => {},
138
- }),
139
- })({});
145
+ },
146
+ scope: {
147
+ channelId: channel.row.id,
148
+ channelName,
149
+ fingerprintHash: null,
150
+ platform: bundle.platform,
151
+ scopeKey,
152
+ strategy: "APP_VERSION",
153
+ },
154
+ });
155
+ return { channel: channel.row, committed, releaseId, repository, scopeKey };
156
+ };
140
157
 
141
158
  describe("Handler <-> Standalone Repository Integration", () => {
142
- it("Real integration: appendBundle + commitBundle → handler POST /bundles", async () => {
143
- // Create standalone repository pointing to our test server
144
- const repo = standaloneRepository({
145
- baseUrl: `${baseUrl}/hot-updater`,
146
- })();
147
-
159
+ it("uses Standalone for bundle management and the server database for Analytics", async () => {
148
160
  const bundleId = uuidv7();
149
- const bundle = createTestBundle({
150
- id: bundleId,
151
- fileHash: "integration-hash-1",
161
+ const installId = "standalone-install";
162
+ await createStandaloneClient().insertBundle(
163
+ createTestBundle({ id: bundleId }),
164
+ );
165
+
166
+ const ingestion = await api.handlers.client(
167
+ new Request(`${baseUrl}/events`, {
168
+ method: "POST",
169
+ headers: {
170
+ "content-type": "application/json",
171
+ },
172
+ body: JSON.stringify({
173
+ appVersion: "1.0.0",
174
+ channel: "production",
175
+ cohort: "default",
176
+ fingerprintHash: null,
177
+ fromBundleId: NIL_UUID,
178
+ fromReleaseId: null,
179
+ installId,
180
+ platform: "ios",
181
+ sdkVersion: "2.0.0",
182
+ toBundleId: bundleId,
183
+ toReleaseId: null,
184
+ type: "UPDATE_APPLIED",
185
+ updateStrategy: "appVersion",
186
+ }),
187
+ }),
188
+ );
189
+ const overview = await api.handlers.admin(
190
+ new Request(`${baseUrl}/installations/overview`),
191
+ );
192
+
193
+ expect(ingestion.status).toBe(204);
194
+ expect(overview.status).toBe(200);
195
+ await expect(overview.json()).resolves.toEqual({
196
+ bundles: [{ bundleId, installations: 1 }],
197
+ trackedInstallations: 1,
152
198
  });
199
+ });
153
200
 
154
- // Standalone repository operations
155
- await repo.appendBundle(bundle);
156
- await repo.commitBundle(); // Triggers actual commit
201
+ it("atomically commits and reads Release catalogs through standalone management routes", async () => {
202
+ const bundle = createTestBundle({ id: uuidv7() });
203
+ const { committed, releaseId, repository, scopeKey } =
204
+ await commitTestRelease(bundle);
157
205
 
158
- // Verify via handler that bundle was created
159
- const request = new Request(
160
- `${baseUrl}/hot-updater/api/bundles/${bundleId}`,
161
- {
162
- method: "GET",
163
- },
206
+ await expect(
207
+ repository.models.releases.findById(releaseId),
208
+ ).resolves.toMatchObject({
209
+ bundle_id: bundle.id,
210
+ revision: 1,
211
+ });
212
+ await expect(
213
+ repository.models.releaseCatalogs.findByScopeKey(scopeKey),
214
+ ).resolves.toMatchObject({ generation: committed.catalog.generation });
215
+ const response = await api.handlers.client(
216
+ new Request(
217
+ `${baseUrl}/release-catalogs/app-version/ios/${encodeChannelKey("production")}/1.0.0`,
218
+ ),
164
219
  );
165
-
166
- const response = await api.handler(request);
167
220
  expect(response.status).toBe(200);
168
-
169
- const retrieved = (await response.json()) as Bundle;
170
- expect(retrieved.id).toBe(bundleId);
171
- expect(retrieved.fileHash).toBe("integration-hash-1");
221
+ await expect(response.json()).resolves.toMatchObject({
222
+ releases: [{ bundleId: bundle.id, releaseId }],
223
+ });
172
224
  });
173
225
 
174
- it("Real integration: getBundleById → handler GET /bundles/:id", async () => {
175
- // First, create a bundle directly via handler
176
- const bundleId = uuidv7();
177
- const bundle = createTestBundle({
178
- id: bundleId,
179
- fileHash: "get-hash-1",
226
+ it("keeps Standalone management routes separate from API key protection", async () => {
227
+ const protectedApi = createHotUpdater({
228
+ database: createInMemoryDatabasePlugin(),
229
+ clientAccess: { type: "api-key" },
180
230
  });
231
+ server.use(
232
+ http.all(`${baseUrl}/protected-hot-updater/*`, async ({ request }) => {
233
+ return invokeHandler(protectedApi, request, "/protected-hot-updater");
234
+ }),
235
+ );
236
+ const bundleId = uuidv7();
237
+ const client = createStandaloneClient(
238
+ `${baseUrl}/protected-hot-updater/admin`,
239
+ );
181
240
 
182
- await api.insertBundle(bundle);
183
-
184
- // Create standalone repository
185
- const repo = standaloneRepository({
186
- baseUrl: `${baseUrl}/hot-updater`,
187
- })();
188
-
189
- // Use standalone repository to retrieve
190
- const retrieved = await repo.getBundleById(bundleId);
241
+ await client.insertBundle(createTestBundle({ id: bundleId }));
242
+ const updateCheck = await protectedApi.handlers.client(
243
+ new Request(
244
+ `${baseUrl}/release-catalogs/app-version/ios/${encodeChannelKey("production")}/1.0.0`,
245
+ ),
246
+ );
191
247
 
192
- expect(retrieved).toBeTruthy();
193
- expect(retrieved?.id).toBe(bundleId);
194
- expect(retrieved?.fileHash).toBe("get-hash-1");
248
+ await expect(client.getBundleById(bundleId)).resolves.toMatchObject({
249
+ id: bundleId,
250
+ });
251
+ expect(updateCheck.status).toBe(401);
195
252
  });
196
253
 
197
- it("Real integration: deleteBundle + commitBundle → handler DELETE /bundles/:id", async () => {
198
- // Create a bundle via handler
254
+ it("creates a bundle through handler POST /bundles", async () => {
199
255
  const bundleId = uuidv7();
200
- const bundle = createTestBundle({
256
+ const response = await api.handlers.admin(
257
+ new Request(`${baseUrl}/bundles`, {
258
+ body: JSON.stringify(
259
+ createTestBundle({
260
+ id: bundleId,
261
+ fileHash: "integration-hash-1",
262
+ }),
263
+ ),
264
+ headers: { "Content-Type": "application/json" },
265
+ method: "POST",
266
+ }),
267
+ );
268
+
269
+ const retrieved = await api.handlers.admin(
270
+ new Request(`${baseUrl}/bundles/${bundleId}`),
271
+ );
272
+ expect(response.status).toBe(201);
273
+ expect(retrieved.status).toBe(200);
274
+ await expect(retrieved.json()).resolves.toMatchObject({
201
275
  id: bundleId,
202
- fileHash: "delete-hash-1",
276
+ fileHash: "integration-hash-1",
203
277
  });
278
+ await expect(api.getReleases({ bundleId, limit: 10 })).resolves.toEqual([]);
279
+ });
204
280
 
205
- await api.insertBundle(bundle);
206
-
207
- // Verify it exists
208
- const beforeDelete = await api.getBundleById(bundleId);
209
- expect(beforeDelete).toBeTruthy();
210
-
211
- // Create standalone repository
212
- const repo = standaloneRepository({
213
- baseUrl: `${baseUrl}/hot-updater`,
214
- })();
281
+ it("retrieves a bundle through handler GET /bundles/:id", async () => {
282
+ const bundleId = uuidv7();
283
+ await api.insertBundle(
284
+ createTestBundle({ id: bundleId, fileHash: "get-hash-1" }),
285
+ );
215
286
 
216
- // Delete via standalone repository
217
- await repo.deleteBundle(bundle);
218
- await repo.commitBundle();
287
+ const retrieved = await createStandaloneClient().getBundleById(bundleId);
219
288
 
220
- // Verify it was deleted
221
- const afterDelete = await api.getBundleById(bundleId);
222
- expect(afterDelete).toBeNull();
289
+ expect(retrieved).toMatchObject({ id: bundleId, fileHash: "get-hash-1" });
223
290
  });
224
291
 
225
- it("Real integration: getBundles → handler GET /bundles", async () => {
226
- // Create multiple bundles
227
- await api.insertBundle(
228
- createTestBundle({ id: uuidv7(), channel: "production" }),
229
- );
230
- await api.insertBundle(
231
- createTestBundle({ id: uuidv7(), channel: "production" }),
232
- );
233
- await api.insertBundle(
234
- createTestBundle({ id: uuidv7(), channel: "staging" }),
235
- );
292
+ it("refuses to delete an artifact referenced by a Release", async () => {
293
+ const bundleId = uuidv7();
294
+ const bundle = createTestBundle({ id: bundleId });
295
+ await commitTestRelease(bundle);
296
+ const client = createStandaloneClient();
236
297
 
237
- // Create standalone repository
238
- const repo = standaloneRepository({
239
- baseUrl: `${baseUrl}/hot-updater`,
240
- })();
298
+ await expect(client.deleteBundleById(bundleId)).rejects.toThrow(
299
+ "still referenced",
300
+ );
241
301
 
242
- // Get all bundles
243
- const result = await repo.getBundles({ limit: 50 });
302
+ await expect(api.getBundleById(bundleId)).resolves.toMatchObject({
303
+ id: bundleId,
304
+ });
305
+ });
244
306
 
245
- expect(result.data).toHaveLength(3);
246
- expect(result.pagination.total).toBe(3);
307
+ it("lists and filters artifacts through handler GET /bundles", async () => {
308
+ await api.insertBundle(createTestBundle({ id: uuidv7() }));
309
+ await api.insertBundle(createTestBundle({ id: uuidv7() }));
310
+ await api.insertBundle(createTestBundle({ id: uuidv7() }));
311
+ const client = createStandaloneClient();
247
312
 
248
- // Filter by channel
249
- const prodResult = await repo.getBundles({
250
- where: { channel: "production" },
313
+ const all = await client.getBundles({ limit: 50 });
314
+ const ios = await client.getBundles({
315
+ where: { platform: "ios" },
251
316
  limit: 50,
252
317
  });
253
318
 
254
- expect(prodResult.data).toHaveLength(2);
319
+ expect(all.data).toHaveLength(3);
320
+ expect(all.pagination.total).toBe(3);
321
+ expect(ios.data).toHaveLength(3);
255
322
  });
256
323
 
257
- it("Real integration: getChannels → handler GET /bundles/channels", async () => {
258
- await api.insertBundle(
259
- createTestBundle({ id: uuidv7(), channel: "production" }),
324
+ it("lists Channel rows through handler GET /channels", async () => {
325
+ await api.insertChannel({
326
+ onConflict: "returnExisting",
327
+ row: { id: uuidv7(), name: "production" },
328
+ });
329
+ await api.insertChannel({
330
+ onConflict: "returnExisting",
331
+ row: { id: uuidv7(), name: "beta" },
332
+ });
333
+
334
+ const channels = await createStandaloneClient().getChannels();
335
+
336
+ expect(channels.map(({ name }) => name)).toEqual(
337
+ expect.arrayContaining(["production", "beta"]),
260
338
  );
261
- await api.insertBundle(createTestBundle({ id: uuidv7(), channel: "beta" }));
262
- await api.insertBundle(
263
- createTestBundle({ id: uuidv7(), channel: "production" }),
339
+ expect(channels.every(({ id }) => id.length > 0)).toBe(true);
340
+ expect(new Set(channels.map(({ name }) => name)).size).toBe(
341
+ channels.length,
264
342
  );
343
+ });
265
344
 
266
- const repo = standaloneRepository({
267
- baseUrl: `${baseUrl}/hot-updater`,
268
- })();
269
-
270
- const channels = await repo.getChannels();
345
+ it("creates and deletes an empty Channel through the canonical routes", async () => {
346
+ const client = createStandaloneClient();
347
+ const id = uuidv7();
271
348
 
272
- expect(channels).toHaveLength(2);
273
- expect(channels).toContain("production");
274
- expect(channels).toContain("beta");
349
+ await expect(
350
+ client.insertChannel({
351
+ row: { id, name: "preview" },
352
+ onConflict: "returnExisting",
353
+ }),
354
+ ).resolves.toEqual({
355
+ row: { id, name: "preview" },
356
+ inserted: true,
357
+ });
358
+ await expect(client.deleteChannel({ id })).resolves.toEqual({
359
+ deleted: true,
360
+ });
361
+ await expect(client.deleteChannel({ id })).resolves.toEqual({
362
+ deleted: false,
363
+ reason: "not_found",
364
+ });
275
365
  });
276
366
 
277
- it("Full E2E: create → retrieve → update → delete via standalone", async () => {
278
- const repo = standaloneRepository({
279
- baseUrl: `${baseUrl}/hot-updater`,
280
- })();
281
-
282
- // Step 1: Create bundle via standalone
367
+ it("refuses to delete a Channel referenced by a Release", async () => {
368
+ const client = createStandaloneClient();
283
369
  const bundleId = uuidv7();
284
- const bundle = createTestBundle({
370
+ await commitTestRelease(createTestBundle({ id: bundleId }), "preview");
371
+ const channel = (await client.getChannels()).find(
372
+ ({ name }) => name === "preview",
373
+ );
374
+
375
+ expect(channel).toBeDefined();
376
+ if (!channel) throw new Error("preview Channel was not created");
377
+ await expect(client.deleteChannel({ id: channel.id })).resolves.toEqual({
378
+ deleted: false,
379
+ reason: "not_empty",
380
+ });
381
+ await expect(client.getBundleById(bundleId)).resolves.toMatchObject({
285
382
  id: bundleId,
286
- fileHash: "e2e-hash",
287
- enabled: true,
288
383
  });
384
+ });
289
385
 
290
- await repo.appendBundle(bundle);
291
- await repo.commitBundle();
386
+ it("updates an artifact without changing its Release or catalog", async () => {
387
+ const bundle = createTestBundle({ id: uuidv7() });
388
+ const { releaseId, repository, scopeKey } = await commitTestRelease(bundle);
389
+ const releaseBefore = await repository.models.releases.findById(releaseId);
390
+ const catalogBefore =
391
+ await repository.models.releaseCatalogs.findByScopeKey(scopeKey);
292
392
 
293
- // Step 2: Retrieve via standalone
294
- const retrieved = await repo.getBundleById(bundleId);
295
- expect(retrieved).toBeTruthy();
296
- expect(retrieved?.enabled).toBe(true);
393
+ await createStandaloneClient().updateBundleById(bundle.id, {
394
+ storageUri: "test://updated-artifact",
395
+ });
297
396
 
298
- // Step 3: Update via standalone
299
- await repo.updateBundle(bundleId, { enabled: false });
300
- await repo.commitBundle();
397
+ await expect(api.getBundleById(bundle.id)).resolves.toMatchObject({
398
+ storageUri: "test://updated-artifact",
399
+ });
400
+ await expect(
401
+ repository.models.releases.findById(releaseId),
402
+ ).resolves.toEqual(releaseBefore);
403
+ await expect(
404
+ repository.models.releaseCatalogs.findByScopeKey(scopeKey),
405
+ ).resolves.toEqual(catalogBefore);
406
+ });
301
407
 
302
- // Verify update
303
- const updated = await repo.getBundleById(bundleId);
304
- expect(updated?.enabled).toBe(false);
408
+ it("creates, retrieves, updates, and deletes through existing routes", async () => {
409
+ const client = createStandaloneClient();
410
+ const bundleId = uuidv7();
411
+ await client.insertBundle(
412
+ createTestBundle({ id: bundleId, fileHash: "e2e-hash" }),
413
+ );
305
414
 
306
- // Step 4: Delete via standalone
307
- await repo.deleteBundle(bundle);
308
- await repo.commitBundle();
415
+ await expect(client.getBundleById(bundleId)).resolves.toMatchObject({
416
+ fileHash: "e2e-hash",
417
+ });
418
+ await client.updateBundleById(bundleId, { storageUri: "test://updated" });
419
+ await expect(client.getBundleById(bundleId)).resolves.toMatchObject({
420
+ storageUri: "test://updated",
421
+ });
422
+ await client.deleteBundleById(bundleId);
423
+ await expect(client.getBundleById(bundleId)).resolves.toBeNull();
424
+ });
425
+
426
+ it("creates multiple artifacts atomically through POST /bundles", async () => {
427
+ const ids = [uuidv7(), uuidv7(), uuidv7()];
309
428
 
310
- // Verify deletion
311
- const deleted = await repo.getBundleById(bundleId);
312
- expect(deleted).toBeNull();
429
+ const response = await api.handlers.admin(
430
+ new Request(`${baseUrl}/bundles`, {
431
+ body: JSON.stringify(ids.map((id) => createTestBundle({ id }))),
432
+ headers: { "Content-Type": "application/json" },
433
+ method: "POST",
434
+ }),
435
+ );
436
+
437
+ expect(response.status).toBe(201);
438
+ for (const id of ids) {
439
+ await expect(api.getBundleById(id)).resolves.toMatchObject({ id });
440
+ }
313
441
  });
314
442
 
315
- it("Multiple bundles in single commit (standalone sends array)", async () => {
316
- const repo = standaloneRepository({
317
- baseUrl: `${baseUrl}/hot-updater`,
318
- })();
319
-
320
- // Append multiple bundles
321
- const bundleId1 = uuidv7();
322
- const bundleId2 = uuidv7();
323
- const bundleId3 = uuidv7();
324
- await repo.appendBundle(createTestBundle({ id: bundleId1 }));
325
- await repo.appendBundle(createTestBundle({ id: bundleId2 }));
326
- await repo.appendBundle(createTestBundle({ id: bundleId3 }));
327
-
328
- // Commit all at once (standalone sends array in POST)
329
- await repo.commitBundle();
330
-
331
- // Verify all were created
332
- const bundle1 = await api.getBundleById(bundleId1);
333
- const bundle2 = await api.getBundleById(bundleId2);
334
- const bundle3 = await api.getBundleById(bundleId3);
335
-
336
- expect(bundle1).toBeTruthy();
337
- expect(bundle2).toBeTruthy();
338
- expect(bundle3).toBeTruthy();
443
+ it("rolls back the entire create batch when one bundle cannot be inserted", async () => {
444
+ const existingId = uuidv7();
445
+ const newId = uuidv7();
446
+ await api.insertBundle(createTestBundle({ id: existingId }));
447
+
448
+ const response = await api.handlers.admin(
449
+ new Request(`${baseUrl}/bundles`, {
450
+ method: "POST",
451
+ headers: { "Content-Type": "application/json" },
452
+ body: JSON.stringify([
453
+ createTestBundle({ id: newId }),
454
+ createTestBundle({ id: existingId }),
455
+ ]),
456
+ }),
457
+ );
458
+
459
+ expect(response.status).toBe(500);
460
+ await expect(api.getBundleById(newId)).resolves.toBeNull();
339
461
  });
340
462
 
341
- it("Works with custom basePath configuration", async () => {
342
- // Create handler with custom basePath
463
+ it("works when handlers are mounted under a custom client path", async () => {
343
464
  const customApi = createHotUpdater({
344
- database: kyselyAdapter({
345
- db: kysely,
346
- provider: "postgresql",
347
- }),
348
- basePath: "/api/v2",
349
- routes: {
350
- updateCheck: true,
351
- bundles: true,
352
- },
465
+ database: kyselyAdapter({ db: kysely, provider: "postgresql" }),
466
+ clientAccess: { type: "public" },
353
467
  });
354
-
355
- // Setup MSW for custom basePath
356
468
  server.use(
357
- http.get(`${baseUrl}/api/v2/*`, async ({ request }) => {
358
- const response = await customApi.handler(request);
359
- return HttpResponse.json(
360
- (await response.json()) as Record<string, unknown>,
361
- {
362
- status: response.status,
363
- },
364
- );
365
- }),
366
- http.post(`${baseUrl}/api/v2/*`, async ({ request }) => {
367
- const response = await customApi.handler(request);
368
- return HttpResponse.json(
369
- (await response.json()) as Record<string, unknown>,
370
- {
371
- status: response.status,
372
- },
373
- );
374
- }),
375
- http.patch(`${baseUrl}/api/v2/*`, async ({ request }) => {
376
- const response = await customApi.handler(request);
377
- return HttpResponse.json(
378
- (await response.json()) as Record<string, unknown>,
379
- {
380
- status: response.status,
381
- },
382
- );
469
+ http.all(`${baseUrl}/api/v2/*`, async ({ request }) => {
470
+ return invokeHandler(customApi, request, "/api/v2");
383
471
  }),
384
472
  );
473
+ const bundleId = uuidv7();
474
+ const client = createStandaloneClient(`${baseUrl}/api/v2/admin`);
385
475
 
386
- // Create standalone repository with matching basePath
387
- const repo = standaloneRepository({
388
- baseUrl: `${baseUrl}/api/v2`,
389
- })();
476
+ await client.insertBundle(
477
+ createTestBundle({ id: bundleId, fileHash: "custom-hash" }),
478
+ );
390
479
 
391
- // Test create and retrieve
392
- const bundleId = uuidv7();
393
- const bundle = createTestBundle({
394
- id: bundleId,
480
+ await expect(client.getBundleById(bundleId)).resolves.toMatchObject({
395
481
  fileHash: "custom-hash",
396
482
  });
397
-
398
- await repo.appendBundle(bundle);
399
- await repo.commitBundle();
400
-
401
- const retrieved = await repo.getBundleById(bundleId);
402
- expect(retrieved).toBeTruthy();
403
- expect(retrieved?.fileHash).toBe("custom-hash");
404
483
  });
405
484
 
406
- it("Handler returns 404 when bundle not found (standalone handles gracefully)", async () => {
407
- const repo = standaloneRepository({
408
- baseUrl: `${baseUrl}/hot-updater`,
409
- })();
410
-
411
- // Try to get non-existent bundle
412
- const result = await repo.getBundleById("non-existent-bundle");
413
-
414
- // Standalone should return null gracefully
415
- expect(result).toBeNull();
485
+ it("returns null when the bundle endpoint returns 404", async () => {
486
+ await expect(
487
+ createStandaloneClient().getBundleById(uuidv7()),
488
+ ).resolves.toBeNull();
416
489
  });
417
490
 
418
- it("updates targetAppVersion through standalone without creating a duplicate blob entry", async () => {
419
- const store: Record<string, string> = {};
420
- const blobApi = createHotUpdater({
421
- database: createInMemoryBlobDatabase(store),
422
- basePath: "/blob-hot-updater",
423
- routes: {
424
- updateCheck: true,
425
- bundles: true,
426
- },
491
+ it("updates a bundle without creating a duplicate row", async () => {
492
+ const memoryApi = createHotUpdater({
493
+ database: createInMemoryDatabasePlugin(),
494
+ clientAccess: { type: "public" },
427
495
  });
428
- const handleBlobRequest = async (request: Request) => {
429
- const response = await blobApi.handler(request);
430
- return HttpResponse.json(
431
- (await response.json()) as Record<string, unknown>,
432
- {
433
- status: response.status,
434
- headers: response.headers,
435
- },
436
- );
437
- };
438
-
439
496
  server.use(
440
- http.get(`${baseUrl}/blob-hot-updater/api/bundles`, ({ request }) =>
441
- handleBlobRequest(request),
442
- ),
443
- http.get(`${baseUrl}/blob-hot-updater/api/bundles/:id`, ({ request }) =>
444
- handleBlobRequest(request),
445
- ),
446
- http.post(`${baseUrl}/blob-hot-updater/api/bundles`, ({ request }) =>
447
- handleBlobRequest(request),
448
- ),
449
- http.patch(`${baseUrl}/blob-hot-updater/api/bundles/:id`, ({ request }) =>
450
- handleBlobRequest(request),
451
- ),
452
- http.delete(
453
- `${baseUrl}/blob-hot-updater/api/bundles/:id`,
454
- ({ request }) => handleBlobRequest(request),
455
- ),
497
+ http.all(`${baseUrl}/memory-hot-updater/*`, async ({ request }) => {
498
+ return invokeHandler(memoryApi, request, "/memory-hot-updater");
499
+ }),
500
+ );
501
+ const client = createStandaloneClient(
502
+ `${baseUrl}/memory-hot-updater/admin`,
456
503
  );
457
-
458
- const repo = standaloneRepository({
459
- baseUrl: `${baseUrl}/blob-hot-updater`,
460
- })();
461
-
462
504
  const bundleId = uuidv7();
463
- await repo.appendBundle(
505
+ await client.insertBundle(
464
506
  createTestBundle({
465
507
  id: bundleId,
466
- platform: "ios",
467
- targetAppVersion: "1.x.x",
468
508
  storageUri: "s3://test-bucket/original.zip",
469
509
  }),
470
510
  );
471
- await repo.commitBundle();
472
-
473
- await repo.updateBundle(bundleId, { targetAppVersion: "1.0.2" });
474
- await repo.commitBundle();
475
-
476
- const updatedBundle = await repo.getBundleById(bundleId);
477
- expect(updatedBundle?.targetAppVersion).toBe("1.0.2");
478
-
479
- expect(store["production/ios/1.x.x/update.json"]).toBeUndefined();
480
511
 
481
- const nextBundles = JSON.parse(
482
- store["production/ios/1.0.2/update.json"] ?? "[]",
483
- ) as Bundle[];
484
- expect(nextBundles).toHaveLength(1);
485
- expect(nextBundles[0]?.id).toBe(bundleId);
486
- expect(nextBundles[0]?.targetAppVersion).toBe("1.0.2");
512
+ await client.updateBundleById(bundleId, {
513
+ storageUri: "s3://test-bucket/updated.zip",
514
+ });
487
515
 
488
- const targetVersions = JSON.parse(
489
- store["production/ios/target-app-versions.json"] ?? "[]",
490
- ) as string[];
491
- expect(targetVersions).toEqual(["1.0.2"]);
516
+ await expect(client.getBundleById(bundleId)).resolves.toMatchObject({
517
+ id: bundleId,
518
+ storageUri: "s3://test-bucket/updated.zip",
519
+ });
520
+ await expect(client.getBundles({ limit: 50 })).resolves.toMatchObject({
521
+ data: [
522
+ expect.objectContaining({
523
+ id: bundleId,
524
+ storageUri: "s3://test-bucket/updated.zip",
525
+ }),
526
+ ],
527
+ pagination: { total: 1 },
528
+ });
492
529
  });
493
530
  });