@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,1027 +1,194 @@
1
- import { readFile } from "fs/promises";
2
-
3
- import type { Bundle } from "@hot-updater/core";
4
- import { NIL_UUID } from "@hot-updater/core";
5
- import type {
6
- DatabasePlugin,
7
- RequestEnvContext,
8
- RuntimeStoragePlugin,
9
- RuntimeStorageProfile,
1
+ import {
2
+ createStoragePlugin,
3
+ type DatabasePlugin,
4
+ type ConfigInput,
10
5
  } from "@hot-updater/plugin-core";
11
- import { createDatabasePlugin } from "@hot-updater/plugin-core";
12
6
  import { describe, expect, expectTypeOf, it, vi } from "vitest";
13
7
 
14
- import type { DatabaseAdapterCapabilities, Migrator } from "./db/types";
8
+ import { createInMemoryDatabasePlugin } from "../../test-utils/test/inMemoryDatabasePlugin";
9
+ import packageJson from "../package.json" with { type: "json" };
15
10
  import { createHotUpdater } from "./index";
16
- import type { HandlerAPI, HandlerOptions, HandlerRoutes } from "./index";
11
+ import type {
12
+ ClientAccessPolicy,
13
+ CreateHotUpdaterOptions,
14
+ HandlerAPI,
15
+ } from "./index";
16
+ import {
17
+ createRuntimeDatabase,
18
+ createSchemaManagedDatabase,
19
+ } from "./runtime.testFixtures";
20
+ import { HOT_UPDATER_SCHEMA_VERSION } from "./schema/types";
17
21
  import { HOT_UPDATER_SERVER_VERSION } from "./version";
18
22
 
19
- const bundle: Bundle = {
20
- id: "00000000-0000-0000-0000-000000000001",
21
- platform: "ios",
22
- shouldForceUpdate: false,
23
- enabled: true,
24
- fileHash: "hash123",
25
- gitCommitHash: null,
26
- message: "Test bundle",
27
- channel: "production",
28
- storageUri: "s3://test-bucket/bundles/bundle.zip",
29
- targetAppVersion: "1.0.0",
30
- fingerprintHash: null,
31
- };
32
-
33
- type TestEnv = {
34
- assetHost: string;
35
- };
36
-
37
- type TestContext = RequestEnvContext<TestEnv>;
38
-
39
- const createRuntimeStorage = (
40
- getDownloadUrl: RuntimeStorageProfile<TestContext>["getDownloadUrl"],
41
- readText: RuntimeStorageProfile<TestContext>["readText"] = async () => null,
42
- ): RuntimeStoragePlugin<TestContext> => ({
43
- name: "testStorage",
44
- supportedProtocol: "s3",
45
- profiles: {
46
- runtime: {
47
- getDownloadUrl,
48
- readText,
49
- },
50
- },
51
- });
52
-
53
- const createSchemaManagedDatabase = (
54
- adapterName: string,
55
- version: string | undefined,
56
- ): DatabasePlugin<TestContext> & DatabaseAdapterCapabilities => ({
57
- name: `${adapterName}Database`,
58
- adapterName,
59
- createMigrator: () =>
60
- ({
61
- async getVersion() {
62
- return version;
63
- },
64
- async getNameVariants() {
65
- return {};
66
- },
67
- async next() {
68
- return undefined;
69
- },
70
- async previous() {
71
- return undefined;
72
- },
73
- async up() {
74
- throw new Error("not implemented");
75
- },
76
- async down() {
77
- throw new Error("not implemented");
78
- },
79
- async migrateTo() {
80
- throw new Error("not implemented");
81
- },
82
- async migrateToLatest() {
83
- throw new Error("not implemented");
84
- },
85
- async migrate() {},
86
- }) as Migrator,
87
- async appendBundle() {},
88
- async commitBundle() {},
89
- async deleteBundle() {},
90
- async getBundleById() {
91
- throw new Error("runtime database should not be queried");
92
- },
93
- async getBundles() {
94
- throw new Error("runtime database should not be queried");
95
- },
96
- async getChannels() {
97
- throw new Error("runtime database should not be queried");
98
- },
99
- async updateBundle() {},
100
- });
23
+ const publicClientAccess = {
24
+ type: "public",
25
+ } satisfies ClientAccessPolicy;
101
26
 
102
27
  describe("runtime createHotUpdater", () => {
103
- it("publishes db tooling subpath and removes the runtime subpath", async () => {
104
- const packageJson = JSON.parse(
105
- await readFile(new URL("../package.json", import.meta.url), "utf-8"),
106
- ) as {
107
- exports: Record<string, unknown>;
108
- };
109
-
110
- expect(packageJson.exports["./db"]).toBeDefined();
111
- expect(packageJson.exports["./runtime"]).toBeUndefined();
112
- });
113
-
114
- it("exports runtime-safe handler types from the root entry", () => {
115
- expectTypeOf<HandlerAPI>().toHaveProperty("getBundles");
116
- expectTypeOf<HandlerOptions>().toHaveProperty("routes");
117
- expectTypeOf<HandlerRoutes>().toEqualTypeOf<{
118
- updateCheck: boolean;
119
- bundles: boolean;
120
- }>();
121
- });
122
-
123
- it("exports the root runtime API without database capabilities", () => {
124
- const database: DatabasePlugin<TestContext> = {
125
- name: "testDatabase",
126
- async appendBundle() {},
127
- async commitBundle() {},
128
- async deleteBundle() {},
129
- async getBundleById() {
130
- return null;
131
- },
132
- async getBundles() {
133
- return {
134
- data: [],
135
- pagination: {
136
- currentPage: 1,
137
- hasNextPage: false,
138
- hasPreviousPage: false,
139
- total: 0,
140
- totalPages: 0,
141
- },
142
- };
143
- },
144
- async getChannels() {
145
- return [];
146
- },
147
- async updateBundle() {},
148
- };
149
-
150
- const hotUpdater = createHotUpdater({ database });
151
-
152
- expect(hotUpdater.basePath).toBe("/api");
153
- expect(hotUpdater.adapterName).toBe("testDatabase");
154
- expect(hotUpdater.handler).toEqual(expect.any(Function));
155
- expect("createMigrator" in hotUpdater).toBe(false);
156
- expect("generateSchema" in hotUpdater).toBe(false);
157
- expectTypeOf(hotUpdater).not.toHaveProperty("createMigrator");
158
- expectTypeOf(hotUpdater).not.toHaveProperty("generateSchema");
159
- });
160
-
161
- it("requires storages to implement the runtime profile", () => {
162
- const database: DatabasePlugin<TestContext> = {
163
- name: "testDatabase",
164
- async appendBundle() {},
165
- async commitBundle() {},
166
- async deleteBundle() {},
167
- async getBundleById() {
168
- return null;
169
- },
170
- async getBundles() {
171
- return {
172
- data: [],
173
- pagination: {
174
- currentPage: 1,
175
- hasNextPage: false,
176
- hasPreviousPage: false,
177
- total: 0,
178
- totalPages: 0,
179
- },
180
- };
181
- },
182
- async getChannels() {
183
- return [];
184
- },
185
- async updateBundle() {},
186
- };
187
- const nodeOnlyStorage = {
188
- name: "nodeOnlyStorage",
189
- supportedProtocol: "s3",
190
- profiles: {
191
- node: {
192
- delete: vi.fn(),
193
- downloadFile: vi.fn(),
194
- exists: vi.fn(async () => false),
195
- upload: vi.fn(),
196
- },
197
- },
198
- };
199
-
28
+ it.each(["authorityId", "catalogId"])("rejects a user-supplied %s", (key) => {
200
29
  expect(() =>
201
30
  createHotUpdater({
202
- database,
203
- storages: [
204
- nodeOnlyStorage as unknown as RuntimeStoragePlugin<TestContext>,
205
- ],
31
+ clientAccess: publicClientAccess,
32
+ database: createInMemoryDatabasePlugin(),
33
+ [key]: "user-controlled",
206
34
  }),
207
- ).toThrow(
208
- 'nodeOnlyStorage does not implement the runtime storage profile for protocol "s3".',
209
- );
35
+ ).toThrow(`Remove ${key}`);
210
36
  });
211
37
 
212
- it("rejects runtime access when a Kysely schema is not initialized", async () => {
213
- const hotUpdater = createHotUpdater({
214
- database: createSchemaManagedDatabase("kysely", undefined),
215
- });
38
+ it("publishes only the supported runtime and database subpaths", () => {
39
+ const packageExports = packageJson.exports;
216
40
 
217
- await expect(hotUpdater.getBundles({ limit: 10 })).rejects.toThrow(
218
- "Hot Updater database schema is not initialized for kysely.",
219
- );
220
- });
41
+ const databaseEntry = packageExports["./db"];
42
+ const hasRuntimeEntry = Object.hasOwn(packageExports, "./runtime");
221
43
 
222
- it("rejects runtime access when a MongoDB schema is stale", async () => {
223
- const hotUpdater = createHotUpdater({
224
- database: createSchemaManagedDatabase("mongodb", "0.21.0"),
225
- });
226
-
227
- await expect(hotUpdater.getChannels()).rejects.toThrow(
228
- "Hot Updater database schema version 0.21.0 is not supported by mongodb.",
229
- );
44
+ expect(databaseEntry).toBeDefined();
45
+ expect(hasRuntimeEntry).toBe(false);
230
46
  });
231
47
 
232
- it("resolves storage URLs with handler context when database fast-path is used", async () => {
233
- const request = new Request(
234
- "https://updates.example.com/api/check-update/app-version/ios/1.0.0/production/" +
235
- `${NIL_UUID}/${NIL_UUID}`,
236
- );
237
- const getBundles = vi.fn<DatabasePlugin<TestContext>["getBundles"]>();
238
- const getUpdateInfo = vi.fn<
239
- NonNullable<DatabasePlugin<TestContext>["getUpdateInfo"]>
240
- >(async () => ({
241
- fileHash: bundle.fileHash,
242
- id: bundle.id,
243
- message: bundle.message,
244
- shouldForceUpdate: bundle.shouldForceUpdate,
245
- status: "UPDATE",
246
- storageUri: bundle.storageUri,
247
- }));
248
- const getDownloadUrl = vi.fn<
249
- RuntimeStorageProfile<TestContext>["getDownloadUrl"]
250
- >(async (_storageUri, context) => {
251
- return {
252
- fileUrl: new URL("/bundle.zip", context?.env?.assetHost).toString(),
253
- };
254
- });
255
-
256
- const database: DatabasePlugin<TestContext> = {
257
- name: "testDatabase",
258
- async appendBundle() {},
259
- async commitBundle() {},
260
- async deleteBundle() {},
261
- async getBundleById(id) {
262
- return id === bundle.id ? bundle : null;
263
- },
264
- getBundles,
265
- getUpdateInfo,
266
- async getChannels() {
267
- return ["production"];
268
- },
269
- async onUnmount() {},
270
- async updateBundle() {},
48
+ it("exports runtime-safe handler types from the root entry", () => {
49
+ expectTypeOf<ConfigInput>().not.toHaveProperty("authorityId");
50
+ expectTypeOf<ConfigInput>().not.toHaveProperty("catalogId");
51
+ expectTypeOf<HandlerAPI>().toHaveProperty("getBundles");
52
+ expectTypeOf<keyof CreateHotUpdaterOptions>().toEqualTypeOf<
53
+ "clientAccess" | "database" | "storage"
54
+ >();
55
+ expectTypeOf<CreateHotUpdaterOptions>().toHaveProperty("clientAccess");
56
+ expectTypeOf<
57
+ CreateHotUpdaterOptions["clientAccess"]
58
+ >().toEqualTypeOf<ClientAccessPolicy>();
59
+ });
60
+
61
+ it("accepts a direct v2 plugin object without exposing maintenance methods", () => {
62
+ const database: DatabasePlugin = {
63
+ ...createInMemoryDatabasePlugin(),
64
+ name: "contextlessTestDatabase",
271
65
  };
272
- const storage = createRuntimeStorage(getDownloadUrl);
66
+ const storage = createStoragePlugin({
67
+ name: "contextlessTestStorage",
68
+ protocol: "s3",
69
+ get: async () => ({ response: null }),
70
+ getDownloadUrl: async () => ({
71
+ url: "https://assets.example.com/bundle.zip",
72
+ }),
73
+ });
273
74
 
274
75
  const hotUpdater = createHotUpdater({
76
+ clientAccess: publicClientAccess,
275
77
  database,
276
- storages: [storage],
277
- basePath: "/api/check-update",
278
- routes: {
279
- updateCheck: true,
280
- bundles: false,
281
- },
78
+ storage: [storage],
282
79
  });
283
80
 
284
- const response = await hotUpdater.handler(request, {
285
- env: {
286
- assetHost: "https://assets.example.com",
287
- },
288
- request,
289
- });
290
-
291
- expect(response.status).toBe(200);
292
- await expect(response.json()).resolves.toEqual({
293
- fileHash: "hash123",
294
- fileUrl: "https://assets.example.com/bundle.zip",
295
- id: "00000000-0000-0000-0000-000000000001",
296
- message: "Test bundle",
297
- shouldForceUpdate: false,
298
- status: "UPDATE",
299
- });
300
- expect(getUpdateInfo).toHaveBeenCalledWith(
301
- {
302
- _updateStrategy: "appVersion",
303
- appVersion: "1.0.0",
304
- bundleId: NIL_UUID,
305
- channel: "production",
306
- cohort: undefined,
307
- minBundleId: NIL_UUID,
308
- platform: "ios",
309
- },
310
- expect.objectContaining({
311
- env: {
312
- assetHost: "https://assets.example.com",
313
- },
314
- request: expect.any(Request),
315
- }),
316
- );
317
- expect(getBundles).not.toHaveBeenCalled();
318
- expect(getDownloadUrl).toHaveBeenCalledWith(
319
- "s3://test-bucket/bundles/bundle.zip",
320
- expect.objectContaining({
321
- env: {
322
- assetHost: "https://assets.example.com",
323
- },
324
- request: expect.any(Request),
325
- }),
326
- );
81
+ expect(hotUpdater.adapterName).toBe("contextlessTestDatabase");
82
+ expect(hotUpdater.handlers.client).toEqual(expect.any(Function));
83
+ expect(hotUpdater.handlers.admin).toEqual(expect.any(Function));
84
+ expect("createMigrator" in hotUpdater).toBe(false);
85
+ expect("generateSchema" in hotUpdater).toBe(false);
86
+ expect(hotUpdater).not.toHaveProperty("authorityId");
87
+ expect(hotUpdater).not.toHaveProperty("catalogId");
88
+ expectTypeOf(hotUpdater).not.toHaveProperty("createMigrator");
89
+ expectTypeOf(hotUpdater).not.toHaveProperty("generateSchema");
90
+ expectTypeOf(hotUpdater.handlers.client)
91
+ .parameter(0)
92
+ .toEqualTypeOf<Request>();
327
93
  });
328
94
 
329
- it("passes the handler context to database and storage resolution", async () => {
330
- const request = new Request(
331
- "https://updates.example.com/api/check-update/app-version/ios/1.0.0/production/" +
332
- `${NIL_UUID}/${NIL_UUID}`,
333
- );
334
- const getBundles = vi.fn<DatabasePlugin<TestContext>["getBundles"]>(
335
- async () => {
336
- return {
337
- data: [bundle],
338
- pagination: {
339
- hasNextPage: false,
340
- hasPreviousPage: false,
341
- currentPage: 1,
342
- totalPages: 1,
343
- total: 1,
344
- },
345
- };
346
- },
347
- );
348
- const getDownloadUrl = vi.fn<
349
- RuntimeStorageProfile<TestContext>["getDownloadUrl"]
350
- >(async (_storageUri, context) => {
351
- return {
352
- fileUrl: new URL("/bundle.zip", context?.env?.assetHost).toString(),
353
- };
354
- });
355
-
356
- const database: DatabasePlugin<TestContext> = {
357
- name: "testDatabase",
358
- async appendBundle() {},
359
- async commitBundle() {},
360
- async deleteBundle() {},
361
- async getBundleById(id) {
362
- return id === bundle.id ? bundle : null;
363
- },
364
- getBundles,
365
- async getChannels() {
366
- return ["production"];
367
- },
368
- async onUnmount() {},
369
- async updateBundle() {},
370
- };
371
- const storage = createRuntimeStorage(getDownloadUrl);
372
-
95
+ it("rejects access when a managed schema is not initialized", async () => {
373
96
  const hotUpdater = createHotUpdater({
374
- database,
375
- storages: [storage],
376
- basePath: "/api/check-update",
377
- routes: {
378
- updateCheck: true,
379
- bundles: false,
380
- },
97
+ clientAccess: publicClientAccess,
98
+ database: createSchemaManagedDatabase("kysely", undefined),
381
99
  });
382
100
 
383
- expectTypeOf(hotUpdater.handler)
384
- .parameter(1)
385
- .toEqualTypeOf<TestContext | undefined>();
386
-
387
- const response = await hotUpdater.handler(request, {
388
- env: {
389
- assetHost: "https://assets.example.com",
390
- },
391
- request,
392
- });
101
+ const result = hotUpdater.getBundles({ limit: 10 });
393
102
 
394
- expect(response.status).toBe(200);
395
- await expect(response.json()).resolves.toEqual({
396
- fileHash: "hash123",
397
- fileUrl: "https://assets.example.com/bundle.zip",
398
- id: "00000000-0000-0000-0000-000000000001",
399
- message: "Test bundle",
400
- shouldForceUpdate: false,
401
- status: "UPDATE",
402
- });
403
- expect(getBundles).toHaveBeenCalledWith(
404
- expect.any(Object),
405
- expect.objectContaining({
406
- env: {
407
- assetHost: "https://assets.example.com",
408
- },
409
- request: expect.any(Request),
410
- }),
411
- );
412
- expect(getDownloadUrl).toHaveBeenCalledWith(
413
- "s3://test-bucket/bundles/bundle.zip",
414
- expect.objectContaining({
415
- env: {
416
- assetHost: "https://assets.example.com",
417
- },
418
- request: expect.any(Request),
419
- }),
103
+ await expect(result).rejects.toThrow(
104
+ "Hot Updater database schema is not initialized for kysely.",
420
105
  );
421
106
  });
422
107
 
423
- it("returns bsdiff patch metadata when the full asset fallback URL is unavailable", async () => {
424
- const currentManifestStorageUri =
425
- "s3://test-bucket/releases/00000000-0000-0000-0000-000000000001/manifest.json";
426
- const nextManifestStorageUri =
427
- "s3://test-bucket/releases/00000000-0000-0000-0000-000000000002/manifest.json";
428
- const currentBundle: Bundle = {
429
- ...bundle,
430
- id: "00000000-0000-0000-0000-000000000001",
431
- assetBaseStorageUri:
432
- "s3://test-bucket/releases/00000000-0000-0000-0000-000000000001/files",
433
- manifestFileHash: "sig:current-manifest",
434
- manifestStorageUri: currentManifestStorageUri,
435
- storageUri:
436
- "s3://test-bucket/releases/00000000-0000-0000-0000-000000000001/bundle.zip",
437
- };
438
- const nextBundle: Bundle = {
439
- ...bundle,
440
- id: "00000000-0000-0000-0000-000000000002",
441
- assetBaseStorageUri:
442
- "s3://test-bucket/releases/00000000-0000-0000-0000-000000000002/files",
443
- manifestFileHash: "sig:next-manifest",
444
- manifestStorageUri: nextManifestStorageUri,
445
- patches: [
446
- {
447
- baseBundleId: currentBundle.id,
448
- baseFileHash: "hash-old-bundle",
449
- patchFileHash: "hash-bsdiff",
450
- patchStorageUri:
451
- "s3://test-bucket/releases/00000000-0000-0000-0000-000000000002/patches/00000000-0000-0000-0000-000000000001/index.ios.bundle.bsdiff",
452
- },
453
- ],
454
- storageUri:
455
- "s3://test-bucket/releases/00000000-0000-0000-0000-000000000002/bundle.zip",
456
- };
457
- const request = new Request(
458
- "https://updates.example.com/api/check-update/app-version/ios/1.0.0/production/" +
459
- `${NIL_UUID}/${currentBundle.id}`,
460
- );
461
- const getBundles = vi.fn<DatabasePlugin<TestContext>["getBundles"]>(
462
- async () => ({
463
- data: [nextBundle],
464
- pagination: {
465
- hasNextPage: false,
466
- hasPreviousPage: false,
467
- currentPage: 1,
468
- totalPages: 1,
469
- total: 1,
470
- },
471
- }),
472
- );
473
- const getDownloadUrl = vi.fn<
474
- RuntimeStorageProfile<TestContext>["getDownloadUrl"]
475
- >(async (storageUri, context) => {
476
- if (storageUri.endsWith("/files/index.ios.bundle")) {
477
- throw new Error("full asset fallback is unavailable");
478
- }
479
-
480
- const storageUrl = new URL(storageUri);
481
- return {
482
- fileUrl: new URL(
483
- storageUrl.pathname,
484
- context?.env?.assetHost,
485
- ).toString(),
486
- };
487
- });
488
- const manifests = new Map([
489
- [
490
- currentManifestStorageUri,
491
- JSON.stringify({
492
- assets: {
493
- "assets/logo.png": {
494
- fileHash: "hash-logo",
495
- },
496
- "index.ios.bundle": {
497
- fileHash: "hash-old-bundle",
498
- },
499
- },
500
- bundleId: currentBundle.id,
501
- }),
502
- ],
503
- [
504
- nextManifestStorageUri,
505
- JSON.stringify({
506
- assets: {
507
- "assets/logo.png": {
508
- fileHash: "hash-logo",
509
- },
510
- "index.ios.bundle": {
511
- fileHash: "hash-new-bundle",
512
- },
513
- },
514
- bundleId: nextBundle.id,
515
- }),
516
- ],
517
- ]);
518
- const readText = vi.fn<RuntimeStorageProfile<TestContext>["readText"]>(
519
- async (storageUri) => manifests.get(storageUri) ?? null,
520
- );
521
- const fetchMock = vi.fn<typeof fetch>(async () => {
522
- return new Response("manifest fetch should not be used", {
523
- status: 500,
108
+ it.each(["s3", "https"])(
109
+ "rejects registered %s storage without getDownloadUrl",
110
+ (protocol) => {
111
+ const storage = createStoragePlugin({
112
+ name: "deployOnlyStorage",
113
+ protocol,
114
+ get: async () => ({ response: null }),
524
115
  });
525
- });
526
- vi.stubGlobal("fetch", fetchMock);
527
116
 
528
- const database: DatabasePlugin<TestContext> = {
529
- name: "testDatabase",
530
- async appendBundle() {},
531
- async commitBundle() {},
532
- async deleteBundle() {},
533
- async getBundleById(id) {
534
- if (id === currentBundle.id) {
535
- return currentBundle;
536
- }
537
- if (id === nextBundle.id) {
538
- return nextBundle;
539
- }
540
- return null;
541
- },
542
- getBundles,
543
- async getChannels() {
544
- return ["production"];
545
- },
546
- async onUnmount() {},
547
- async updateBundle() {},
548
- };
549
- const storage = createRuntimeStorage(getDownloadUrl, readText);
550
-
551
- try {
552
- const hotUpdater = createHotUpdater({
553
- database,
554
- storages: [storage],
555
- basePath: "/api/check-update",
556
- routes: {
557
- updateCheck: true,
558
- bundles: false,
559
- },
560
- });
561
-
562
- const response = await hotUpdater.handler(request, {
563
- env: {
564
- assetHost: "https://assets.example.com",
565
- },
566
- request,
567
- });
568
-
569
- await expect(response.json()).resolves.toEqual({
570
- changedAssets: {
571
- "index.ios.bundle": {
572
- file: {
573
- compression: "br",
574
- url: "https://assets.example.com/releases/00000000-0000-0000-0000-000000000002/files/index.ios.bundle.br",
575
- },
576
- fileHash: "hash-new-bundle",
577
- patch: {
578
- algorithm: "bsdiff",
579
- baseBundleId: "00000000-0000-0000-0000-000000000001",
580
- baseFileHash: "hash-old-bundle",
581
- patchFileHash: "hash-bsdiff",
582
- patchUrl:
583
- "https://assets.example.com/releases/00000000-0000-0000-0000-000000000002/patches/00000000-0000-0000-0000-000000000001/index.ios.bundle.bsdiff",
584
- },
585
- },
586
- },
587
- fileHash: "hash123",
588
- fileUrl:
589
- "https://assets.example.com/releases/00000000-0000-0000-0000-000000000002/bundle.zip",
590
- id: "00000000-0000-0000-0000-000000000002",
591
- manifestFileHash: "sig:next-manifest",
592
- manifestUrl:
593
- "https://assets.example.com/releases/00000000-0000-0000-0000-000000000002/manifest.json",
594
- message: "Test bundle",
595
- shouldForceUpdate: false,
596
- status: "UPDATE",
597
- });
598
- expect(readText).toHaveBeenCalledWith(
599
- nextManifestStorageUri,
600
- expect.objectContaining({
601
- env: {
602
- assetHost: "https://assets.example.com",
603
- },
604
- request: expect.any(Request),
117
+ expect(() =>
118
+ createHotUpdater({
119
+ clientAccess: publicClientAccess,
120
+ database: createRuntimeDatabase(),
121
+ storage: [storage],
605
122
  }),
123
+ ).toThrow(
124
+ 'Storage plugin "deployOnlyStorage" does not implement getDownloadUrl.',
606
125
  );
607
- expect(readText).toHaveBeenCalledWith(
608
- currentManifestStorageUri,
609
- expect.objectContaining({
610
- env: {
611
- assetHost: "https://assets.example.com",
612
- },
613
- request: expect.any(Request),
614
- }),
615
- );
616
- expect(fetchMock).not.toHaveBeenCalled();
617
- } finally {
618
- vi.unstubAllGlobals();
619
- }
620
- });
621
-
622
- it("does not inject the request into context unless explicitly provided", async () => {
623
- const getBundles = vi.fn<DatabasePlugin<TestContext>["getBundles"]>(
624
- async () => {
625
- return {
626
- data: [bundle],
627
- pagination: {
628
- hasNextPage: false,
629
- hasPreviousPage: false,
630
- currentPage: 1,
631
- totalPages: 1,
632
- total: 1,
633
- },
634
- };
635
- },
636
- );
637
-
638
- const database: DatabasePlugin<TestContext> = {
639
- name: "testDatabase",
640
- async appendBundle() {},
641
- async commitBundle() {},
642
- async deleteBundle() {},
643
- async getBundleById(id) {
644
- return id === bundle.id ? bundle : null;
645
- },
646
- getBundles,
647
- async getChannels() {
648
- return ["production"];
649
- },
650
- async onUnmount() {},
651
- async updateBundle() {},
652
- };
653
- const storage = createRuntimeStorage(async () => {
654
- return { fileUrl: "https://assets.example.com/bundle.zip" };
655
- });
126
+ },
127
+ );
656
128
 
129
+ it("rejects access when a managed schema is stale", async () => {
657
130
  const hotUpdater = createHotUpdater({
658
- database,
659
- storages: [storage],
660
- basePath: "/api/check-update",
661
- routes: {
662
- updateCheck: true,
663
- bundles: false,
664
- },
131
+ clientAccess: publicClientAccess,
132
+ database: createSchemaManagedDatabase("mongodb", "0.21.0"),
665
133
  });
666
134
 
667
- const response = await hotUpdater.handler(
668
- new Request(
669
- "https://updates.example.com/api/check-update/app-version/ios/1.0.0/production/" +
670
- `${NIL_UUID}/${NIL_UUID}`,
671
- ),
672
- {
673
- env: {
674
- assetHost: "https://assets.example.com",
675
- },
676
- },
677
- );
135
+ const result = hotUpdater.getChannels();
678
136
 
679
- expect(response.status).toBe(200);
680
- expect(getBundles).toHaveBeenCalledWith(expect.any(Object), {
681
- env: {
682
- assetHost: "https://assets.example.com",
683
- },
684
- });
137
+ await expect(result).rejects.toThrow(
138
+ "Hot Updater v1 cannot migrate schema 0.21.0 in place.",
139
+ );
685
140
  });
686
141
 
687
- it("supports stripped base-path requests and ignores extra framework args", async () => {
688
- const getBundles = vi.fn<DatabasePlugin<TestContext>["getBundles"]>(
689
- async () => {
690
- return {
691
- data: [bundle],
692
- pagination: {
693
- hasNextPage: false,
694
- hasPreviousPage: false,
695
- currentPage: 1,
696
- totalPages: 1,
697
- total: 1,
698
- },
699
- };
700
- },
142
+ it("checks a ready managed schema only once", async () => {
143
+ const database = createSchemaManagedDatabase(
144
+ "kysely",
145
+ HOT_UPDATER_SCHEMA_VERSION,
701
146
  );
702
-
703
- const database: DatabasePlugin<TestContext> = {
704
- name: "testDatabase",
705
- async appendBundle() {},
706
- async commitBundle() {},
707
- async deleteBundle() {},
708
- async getBundleById(id) {
709
- return id === bundle.id ? bundle : null;
710
- },
711
- getBundles,
712
- async getChannels() {
713
- return ["production"];
714
- },
715
- async onUnmount() {},
716
- async updateBundle() {},
717
- };
718
- const storage = createRuntimeStorage(async () => {
719
- return { fileUrl: "https://assets.example.com/bundle.zip" };
720
- });
721
-
147
+ const createMigrator = vi.spyOn(database, "createMigrator");
722
148
  const hotUpdater = createHotUpdater({
149
+ clientAccess: publicClientAccess,
723
150
  database,
724
- storages: [storage],
725
- basePath: "/api/check-update",
726
- routes: {
727
- updateCheck: true,
728
- bundles: false,
729
- },
730
151
  });
731
152
 
732
- const mountStyleHandler = hotUpdater.handler as (
733
- request: Request,
734
- context?: unknown,
735
- executionCtx?: unknown,
736
- ) => Promise<Response>;
737
- const response = await mountStyleHandler(
738
- new Request(
739
- "https://updates.example.com/app-version/ios/1.0.0/production/" +
740
- `${NIL_UUID}/${NIL_UUID}`,
741
- ),
742
- { someBinding: "ignored" },
743
- { waitUntil: () => undefined },
744
- );
153
+ await hotUpdater.getChannels();
154
+ await hotUpdater.getChannels();
745
155
 
746
- expect(response.status).toBe(200);
747
- expect(getBundles).toHaveBeenCalledWith(expect.any(Object), undefined);
156
+ expect(createMigrator).toHaveBeenCalledOnce();
748
157
  });
749
158
 
750
- it("keeps the version route mounted when bundle routes are disabled", async () => {
751
- const database = createDatabasePlugin({
752
- name: "version-enabled-plugin",
753
- factory: () => ({
754
- async getBundleById() {
755
- return null;
756
- },
757
- async getBundles() {
758
- return {
759
- data: [],
760
- pagination: {
761
- hasNextPage: false,
762
- hasPreviousPage: false,
763
- currentPage: 1,
764
- totalPages: 1,
765
- total: 0,
766
- },
767
- };
768
- },
769
- async getChannels() {
770
- return [];
771
- },
772
- async commitBundle() {},
773
- }),
774
- })({});
775
-
159
+ it("keeps the version route mounted on the client handler", async () => {
776
160
  const hotUpdater = createHotUpdater({
777
- database,
778
- basePath: "/api/check-update",
779
- routes: {
780
- updateCheck: true,
781
- bundles: false,
782
- },
161
+ clientAccess: publicClientAccess,
162
+ database: createRuntimeDatabase(),
783
163
  });
784
164
 
785
- const response = await hotUpdater.handler(
786
- new Request("https://updates.example.com/api/check-update/version"),
165
+ const response = await hotUpdater.handlers.client(
166
+ new Request("https://updates.example.com/version"),
787
167
  );
788
168
 
789
169
  expect(response.status).toBe(200);
790
170
  await expect(response.json()).resolves.toEqual({
171
+ infrastructureGeneration: 1,
791
172
  version: HOT_UPDATER_SERVER_VERSION,
792
173
  });
793
174
  });
794
175
 
795
- it("keeps the version route mounted when update-check routes are disabled", async () => {
796
- const database = createDatabasePlugin({
797
- name: "version-disabled-plugin",
798
- factory: () => ({
799
- async getBundleById() {
800
- return null;
801
- },
802
- async getBundles() {
803
- return {
804
- data: [],
805
- pagination: {
806
- hasNextPage: false,
807
- hasPreviousPage: false,
808
- currentPage: 1,
809
- totalPages: 1,
810
- total: 0,
811
- },
812
- };
813
- },
814
- async getChannels() {
815
- return [];
816
- },
817
- async commitBundle() {},
818
- }),
819
- })({});
820
-
821
- const hotUpdater = createHotUpdater({
822
- database,
823
- basePath: "/api/check-update",
824
- routes: {
825
- updateCheck: false,
826
- bundles: false,
827
- },
828
- });
829
-
830
- const response = await hotUpdater.handler(
831
- new Request("https://updates.example.com/api/check-update/version"),
832
- );
833
-
834
- expect(response.status).toBe(200);
835
- await expect(response.json()).resolves.toEqual({
836
- version: HOT_UPDATER_SERVER_VERSION,
837
- });
838
- });
839
-
840
- it("keeps optional maintenance capabilities lazy", () => {
841
- const factory = vi.fn(() => ({
842
- async getBundleById() {
843
- return null;
844
- },
845
- async getBundles() {
846
- return {
847
- data: [],
848
- pagination: {
849
- hasNextPage: false,
850
- hasPreviousPage: false,
851
- currentPage: 1,
852
- totalPages: 1,
853
- total: 0,
854
- },
855
- };
856
- },
857
- async getChannels() {
858
- return [];
859
- },
860
- async commitBundle() {},
861
- }));
862
- const database = createDatabasePlugin({
863
- name: "lazyRuntimePlugin",
864
- factory,
865
- })({});
866
-
867
- createHotUpdater({
868
- database,
869
- basePath: "/api/check-update",
870
- });
871
-
872
- expect(factory).not.toHaveBeenCalled();
873
- });
874
-
875
- it("clears pending plugin changes after a failed mutation commit", async () => {
876
- const committedBundles = new Map<string, Bundle>();
877
- let commitAttempt = 0;
878
-
879
- const database = createDatabasePlugin({
880
- name: "failingPlugin",
881
- factory: () => ({
882
- async getBundleById(bundleId) {
883
- return committedBundles.get(bundleId) ?? null;
884
- },
885
- async getBundles() {
886
- return {
887
- data: Array.from(committedBundles.values()),
888
- pagination: {
889
- hasNextPage: false,
890
- hasPreviousPage: false,
891
- currentPage: 1,
892
- totalPages: 1,
893
- total: committedBundles.size,
894
- },
895
- };
896
- },
897
- async getChannels() {
898
- return [];
899
- },
900
- async commitBundle({ changedSets }) {
901
- commitAttempt += 1;
902
-
903
- if (commitAttempt === 1) {
904
- throw new Error("commit failed");
905
- }
906
-
907
- for (const change of changedSets) {
908
- if (change.operation === "delete") {
909
- committedBundles.delete(change.data.id);
910
- continue;
911
- }
912
-
913
- committedBundles.set(change.data.id, change.data);
914
- }
915
- },
916
- }),
917
- })({});
918
-
919
- const hotUpdater = createHotUpdater({
920
- database,
921
- basePath: "/api/check-update",
922
- routes: {
923
- updateCheck: false,
924
- bundles: false,
925
- },
926
- });
927
-
928
- const failedBundle: Bundle = {
929
- ...bundle,
930
- id: "00000000-0000-0000-0000-000000000010",
931
- message: "failed bundle",
932
- };
933
- const succeedingBundle: Bundle = {
934
- ...bundle,
935
- id: "00000000-0000-0000-0000-000000000011",
936
- message: "succeeding bundle",
937
- };
938
-
939
- await expect(hotUpdater.insertBundle(failedBundle)).rejects.toThrow(
940
- "commit failed",
941
- );
942
- await hotUpdater.insertBundle(succeedingBundle);
943
-
944
- expect(await hotUpdater.getBundleById(failedBundle.id)).toBeNull();
945
- await expect(
946
- hotUpdater.getBundleById(succeedingBundle.id),
947
- ).resolves.toEqual(succeedingBundle);
176
+ it("rejects a missing client access policy", () => {
177
+ expect(() =>
178
+ createHotUpdater({
179
+ database: createRuntimeDatabase(),
180
+ } as unknown as CreateHotUpdaterOptions),
181
+ ).toThrow("clientAccess must be an object.");
948
182
  });
949
183
 
950
- it("isolates pending mutation state between overlapping writes", async () => {
951
- const committedBundleIds: string[][] = [];
952
- const onUnmount = vi.fn(async () => undefined);
953
- let releaseFirstCommit!: () => void;
954
- let notifyFirstCommitStarted!: () => void;
955
- const firstCommitStarted = new Promise<void>((resolve) => {
956
- notifyFirstCommitStarted = resolve;
957
- });
958
- const firstCommitGate = new Promise<void>((resolve) => {
959
- releaseFirstCommit = resolve;
960
- });
961
- let commitCount = 0;
962
-
963
- const database = createDatabasePlugin({
964
- name: "isolatedPlugin",
965
- factory: () => ({
966
- async getBundleById() {
967
- return null;
968
- },
969
- async getBundles() {
970
- return {
971
- data: [],
972
- pagination: {
973
- hasNextPage: false,
974
- hasPreviousPage: false,
975
- currentPage: 1,
976
- totalPages: 1,
977
- total: 0,
978
- },
979
- };
980
- },
981
- async getChannels() {
982
- return [];
983
- },
984
- onUnmount,
985
- async commitBundle({ changedSets }) {
986
- commitCount += 1;
987
- committedBundleIds.push(changedSets.map((change) => change.data.id));
988
-
989
- if (commitCount === 1) {
990
- notifyFirstCommitStarted();
991
- await firstCommitGate;
992
- }
184
+ it("rejects an unsupported client access policy", () => {
185
+ expect(() =>
186
+ createHotUpdater({
187
+ clientAccess: {
188
+ type: "private" as unknown as "public",
993
189
  },
190
+ database: createRuntimeDatabase(),
994
191
  }),
995
- })({});
996
-
997
- const hotUpdater = createHotUpdater({
998
- database,
999
- basePath: "/api/check-update",
1000
- routes: {
1001
- updateCheck: false,
1002
- bundles: false,
1003
- },
1004
- });
1005
-
1006
- const firstBundleId = "00000000-0000-0000-0000-000000000020";
1007
- const secondBundleId = "00000000-0000-0000-0000-000000000021";
1008
-
1009
- const firstInsert = hotUpdater.insertBundle({
1010
- ...bundle,
1011
- id: firstBundleId,
1012
- message: "first bundle",
1013
- });
1014
- await firstCommitStarted;
1015
-
1016
- const secondInsert = hotUpdater.insertBundle({
1017
- ...bundle,
1018
- id: secondBundleId,
1019
- message: "second bundle",
1020
- });
1021
-
1022
- releaseFirstCommit();
1023
- await Promise.all([firstInsert, secondInsert]);
1024
-
1025
- expect(committedBundleIds).toEqual([[firstBundleId], [secondBundleId]]);
192
+ ).toThrow('clientAccess.type must be either "public" or "api-key".');
1026
193
  });
1027
194
  });