@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,1095 +0,0 @@
1
- import type { Bundle, GetBundlesArgs, UpdateInfo } from "@hot-updater/core";
2
- import { NIL_UUID } from "@hot-updater/core";
3
- import type {
4
- DatabasePlugin,
5
- RequestEnvContext,
6
- } from "@hot-updater/plugin-core";
7
- import { resolveUpdateInfoFromBundles } from "@hot-updater/plugin-core";
8
- import { describe, expect, it, vi } from "vitest";
9
-
10
- import { createPluginDatabaseCore } from "./pluginCore";
11
-
12
- const baseBundle: Bundle = {
13
- id: "00000000-0000-0000-0000-000000000001",
14
- channel: "production",
15
- enabled: true,
16
- fileHash: "hash-1",
17
- fingerprintHash: null,
18
- gitCommitHash: null,
19
- message: "bundle",
20
- platform: "ios",
21
- shouldForceUpdate: false,
22
- storageUri: "s3://bucket/bundle.zip",
23
- targetAppVersion: "1.0.0",
24
- };
25
-
26
- const updateArgs: GetBundlesArgs = {
27
- _updateStrategy: "appVersion",
28
- appVersion: "1.0.0",
29
- bundleId: NIL_UUID,
30
- platform: "ios",
31
- };
32
-
33
- type TestContext = RequestEnvContext<{
34
- assetHost: string;
35
- }>;
36
-
37
- describe("createPluginDatabaseCore", () => {
38
- it("prefers plugin getUpdateInfo fast-path when provided", async () => {
39
- const getBundles = vi.fn<DatabasePlugin<TestContext>["getBundles"]>();
40
- const expected: UpdateInfo = {
41
- fileHash: baseBundle.fileHash,
42
- id: baseBundle.id,
43
- message: baseBundle.message,
44
- shouldForceUpdate: baseBundle.shouldForceUpdate,
45
- status: "UPDATE",
46
- storageUri: baseBundle.storageUri,
47
- };
48
- const getUpdateInfo = vi.fn<
49
- NonNullable<DatabasePlugin<TestContext>["getUpdateInfo"]>
50
- >(async () => expected);
51
-
52
- const plugin: DatabasePlugin<TestContext> = {
53
- name: "fast-path-plugin",
54
- async appendBundle() {},
55
- async commitBundle() {},
56
- async deleteBundle() {},
57
- async getBundleById() {
58
- return null;
59
- },
60
- getBundles,
61
- getUpdateInfo,
62
- async getChannels() {
63
- return ["production"];
64
- },
65
- async updateBundle() {},
66
- };
67
-
68
- const core = createPluginDatabaseCore(
69
- () => plugin,
70
- async () => null,
71
- );
72
- const context: TestContext = {
73
- env: {
74
- assetHost: "https://assets.example.com",
75
- },
76
- request: new Request("https://updates.example.com"),
77
- };
78
-
79
- await expect(core.api.getUpdateInfo(updateArgs, context)).resolves.toEqual(
80
- expected,
81
- );
82
- expect(getUpdateInfo).toHaveBeenCalledWith(updateArgs, context);
83
- expect(getBundles).not.toHaveBeenCalled();
84
- });
85
-
86
- it("resolves manifest artifacts through storage text reader", async () => {
87
- const currentBundle = {
88
- ...baseBundle,
89
- id: "00000000-0000-0000-0000-000000000001",
90
- manifestStorageUri: "r2://bucket/current/manifest.json",
91
- manifestFileHash: "sig:current-manifest",
92
- assetBaseStorageUri: "r2://bucket/current/files",
93
- };
94
- const targetBundle = {
95
- ...baseBundle,
96
- id: "00000000-0000-0000-0000-000000000002",
97
- fileHash: "hash-2",
98
- manifestStorageUri: "r2://bucket/target/manifest.json",
99
- manifestFileHash: "sig:target-manifest",
100
- assetBaseStorageUri: "r2://bucket/target/files",
101
- };
102
- const manifests = new Map([
103
- [
104
- currentBundle.manifestStorageUri,
105
- JSON.stringify({
106
- bundleId: currentBundle.id,
107
- assets: {
108
- "index.ios.bundle": {
109
- fileHash: "old-bundle-hash",
110
- },
111
- },
112
- }),
113
- ],
114
- [
115
- targetBundle.manifestStorageUri,
116
- JSON.stringify({
117
- bundleId: targetBundle.id,
118
- assets: {
119
- "index.ios.bundle": {
120
- fileHash: "new-bundle-hash",
121
- },
122
- },
123
- }),
124
- ],
125
- ]);
126
- const getUpdateInfo = vi.fn<
127
- NonNullable<DatabasePlugin<TestContext>["getUpdateInfo"]>
128
- >(async () => ({
129
- fileHash: targetBundle.fileHash,
130
- id: targetBundle.id,
131
- message: targetBundle.message,
132
- shouldForceUpdate: targetBundle.shouldForceUpdate,
133
- status: "UPDATE",
134
- storageUri: targetBundle.storageUri,
135
- }));
136
-
137
- const plugin: DatabasePlugin<TestContext> = {
138
- name: "manifest-plugin",
139
- async appendBundle() {},
140
- async commitBundle() {},
141
- async deleteBundle() {},
142
- async getBundleById(bundleId) {
143
- if (bundleId === currentBundle.id) return currentBundle;
144
- if (bundleId === targetBundle.id) return targetBundle;
145
- return null;
146
- },
147
- getUpdateInfo,
148
- async getBundles() {
149
- return {
150
- data: [targetBundle],
151
- pagination: {
152
- currentPage: 1,
153
- hasNextPage: false,
154
- hasPreviousPage: false,
155
- total: 1,
156
- totalPages: 1,
157
- },
158
- };
159
- },
160
- async getChannels() {
161
- return ["production"];
162
- },
163
- async updateBundle() {},
164
- };
165
-
166
- const core = createPluginDatabaseCore(
167
- () => plugin,
168
- async (storageUri) => {
169
- if (!storageUri) return null;
170
- const url = new URL(storageUri);
171
- return `https://assets.example.com/${url.host}${url.pathname}`;
172
- },
173
- {
174
- readStorageText: async (storageUri) =>
175
- manifests.get(storageUri) ?? null,
176
- },
177
- );
178
-
179
- await expect(
180
- core.api.getAppUpdateInfo({
181
- ...updateArgs,
182
- bundleId: currentBundle.id,
183
- }),
184
- ).resolves.toMatchObject({
185
- changedAssets: {
186
- "index.ios.bundle": {
187
- file: {
188
- compression: "br",
189
- url: "https://assets.example.com/bucket/target/files/index.ios.bundle.br",
190
- },
191
- fileHash: "new-bundle-hash",
192
- },
193
- },
194
- manifestFileHash: "sig:target-manifest",
195
- manifestUrl: "https://assets.example.com/bucket/target/manifest.json",
196
- });
197
- });
198
-
199
- it("uses request bundle identity map for manifest artifact lookups", async () => {
200
- const currentBundle = {
201
- ...baseBundle,
202
- id: "00000000-0000-0000-0000-000000000001",
203
- manifestStorageUri: "r2://bucket/current/manifest.json",
204
- manifestFileHash: "sig:current-manifest",
205
- assetBaseStorageUri: "r2://bucket/current/files",
206
- };
207
- const targetBundle = {
208
- ...baseBundle,
209
- id: "00000000-0000-0000-0000-000000000002",
210
- fileHash: "hash-2",
211
- manifestStorageUri: "r2://bucket/target/manifest.json",
212
- manifestFileHash: "sig:target-manifest",
213
- assetBaseStorageUri: "r2://bucket/target/files",
214
- };
215
- const manifests = new Map([
216
- [
217
- currentBundle.manifestStorageUri,
218
- JSON.stringify({
219
- bundleId: currentBundle.id,
220
- assets: {
221
- "index.ios.bundle": {
222
- fileHash: "old-bundle-hash",
223
- },
224
- "shared.png": {
225
- fileHash: "same-image-hash",
226
- },
227
- },
228
- }),
229
- ],
230
- [
231
- targetBundle.manifestStorageUri,
232
- JSON.stringify({
233
- bundleId: targetBundle.id,
234
- assets: {
235
- "index.ios.bundle": {
236
- fileHash: "target-bundle-hash",
237
- },
238
- "shared.png": {
239
- fileHash: "same-image-hash",
240
- },
241
- },
242
- }),
243
- ],
244
- ]);
245
- const getBundleById = vi.fn<DatabasePlugin<TestContext>["getBundleById"]>(
246
- async (bundleId) => {
247
- if (bundleId === currentBundle.id) return currentBundle;
248
- if (bundleId === targetBundle.id) return targetBundle;
249
- return null;
250
- },
251
- );
252
- const getUpdateInfo = vi.fn<
253
- NonNullable<DatabasePlugin<TestContext>["getUpdateInfo"]>
254
- >(async () => ({
255
- fileHash: targetBundle.fileHash,
256
- id: targetBundle.id,
257
- message: targetBundle.message,
258
- shouldForceUpdate: targetBundle.shouldForceUpdate,
259
- status: "UPDATE",
260
- storageUri: targetBundle.storageUri,
261
- }));
262
-
263
- const plugin: DatabasePlugin<TestContext> = {
264
- name: "identity-map-plugin",
265
- async appendBundle() {},
266
- async commitBundle() {},
267
- async deleteBundle() {},
268
- getBundleById,
269
- getUpdateInfo,
270
- async getBundles() {
271
- return {
272
- data: [targetBundle],
273
- pagination: {
274
- currentPage: 1,
275
- hasNextPage: false,
276
- hasPreviousPage: false,
277
- total: 1,
278
- totalPages: 1,
279
- },
280
- };
281
- },
282
- async getChannels() {
283
- return ["production"];
284
- },
285
- async updateBundle() {},
286
- };
287
-
288
- const core = createPluginDatabaseCore(
289
- () => plugin,
290
- async (storageUri) => {
291
- if (!storageUri) return null;
292
- const url = new URL(storageUri);
293
- return `https://assets.example.com/${url.host}${url.pathname}`;
294
- },
295
- {
296
- readStorageText: async (storageUri) =>
297
- manifests.get(storageUri) ?? null,
298
- },
299
- );
300
-
301
- const updateInfo = await core.api.getAppUpdateInfo({
302
- ...updateArgs,
303
- bundleId: currentBundle.id,
304
- });
305
- expect(updateInfo).not.toBeNull();
306
- if (!updateInfo) {
307
- throw new Error("expected app update info");
308
- }
309
-
310
- expect(updateInfo).toMatchObject({
311
- changedAssets: {
312
- "index.ios.bundle": {
313
- file: {
314
- compression: "br",
315
- url: "https://assets.example.com/bucket/target/files/index.ios.bundle.br",
316
- },
317
- fileHash: "target-bundle-hash",
318
- },
319
- },
320
- manifestFileHash: "sig:target-manifest",
321
- manifestUrl: "https://assets.example.com/bucket/target/manifest.json",
322
- });
323
- expect(updateInfo.changedAssets).not.toHaveProperty("shared.png");
324
- expect(getUpdateInfo).toHaveBeenCalledOnce();
325
- expect(getBundleById).toHaveBeenCalledTimes(2);
326
- expect(getBundleById).toHaveBeenCalledWith(targetBundle.id, undefined);
327
- expect(getBundleById).toHaveBeenCalledWith(currentBundle.id, undefined);
328
- expect(Object.keys(updateInfo)).not.toContain("__hotUpdaterBundle");
329
- expect(Object.keys(updateInfo)).not.toContain("__hotUpdaterCurrentBundle");
330
- expect(JSON.stringify(updateInfo)).not.toContain("__hotUpdaterBundle");
331
- expect(JSON.stringify(updateInfo)).not.toContain(
332
- "__hotUpdaterCurrentBundle",
333
- );
334
- });
335
-
336
- it("dedupes manifest artifact bundle lookups without request context", async () => {
337
- const targetBundle = {
338
- ...baseBundle,
339
- id: "00000000-0000-0000-0000-000000000002",
340
- fileHash: "hash-2",
341
- manifestStorageUri: "s3://bucket/target/manifest.json",
342
- manifestFileHash: "sig:target-manifest",
343
- assetBaseStorageUri: "s3://bucket/target/files",
344
- };
345
- const manifests = new Map([
346
- [
347
- targetBundle.manifestStorageUri,
348
- JSON.stringify({
349
- bundleId: targetBundle.id,
350
- assets: {
351
- "index.ios.bundle": {
352
- fileHash: "target-bundle-hash",
353
- },
354
- },
355
- }),
356
- ],
357
- ]);
358
- const getBundleById = vi.fn<DatabasePlugin<TestContext>["getBundleById"]>(
359
- async (bundleId) => (bundleId === targetBundle.id ? targetBundle : null),
360
- );
361
- const getUpdateInfo = vi.fn<
362
- NonNullable<DatabasePlugin<TestContext>["getUpdateInfo"]>
363
- >(async () => ({
364
- fileHash: targetBundle.fileHash,
365
- id: targetBundle.id,
366
- message: targetBundle.message,
367
- shouldForceUpdate: targetBundle.shouldForceUpdate,
368
- status: "UPDATE",
369
- storageUri: targetBundle.storageUri,
370
- }));
371
-
372
- const plugin: DatabasePlugin<TestContext> = {
373
- name: "undefined-context-identity-map-plugin",
374
- async appendBundle() {},
375
- async commitBundle() {},
376
- async deleteBundle() {},
377
- getBundleById,
378
- getUpdateInfo,
379
- async getBundles() {
380
- return {
381
- data: [targetBundle],
382
- pagination: {
383
- currentPage: 1,
384
- hasNextPage: false,
385
- hasPreviousPage: false,
386
- total: 1,
387
- totalPages: 1,
388
- },
389
- };
390
- },
391
- async getChannels() {
392
- return ["production"];
393
- },
394
- async updateBundle() {},
395
- };
396
-
397
- const core = createPluginDatabaseCore(
398
- () => plugin,
399
- async (storageUri) => {
400
- if (!storageUri) return null;
401
- const url = new URL(storageUri);
402
- return `https://assets.example.com/${url.host}${url.pathname}`;
403
- },
404
- {
405
- readStorageText: async (storageUri) =>
406
- manifests.get(storageUri) ?? null,
407
- },
408
- );
409
-
410
- const updateInfo = await core.api.getAppUpdateInfo({
411
- ...updateArgs,
412
- bundleId: targetBundle.id,
413
- });
414
-
415
- expect(updateInfo).toMatchObject({
416
- changedAssets: {},
417
- id: targetBundle.id,
418
- manifestFileHash: "sig:target-manifest",
419
- manifestUrl: "https://assets.example.com/bucket/target/manifest.json",
420
- });
421
- expect(getBundleById).toHaveBeenCalledOnce();
422
- expect(getBundleById).toHaveBeenCalledWith(targetBundle.id, undefined);
423
- });
424
-
425
- it("seeds request bundle identity map from provider update lookups", async () => {
426
- const targetBundle = {
427
- ...baseBundle,
428
- id: "00000000-0000-0000-0000-000000000002",
429
- fileHash: "hash-2",
430
- manifestStorageUri: "s3://bucket/target/manifest.json",
431
- manifestFileHash: "sig:target-manifest",
432
- assetBaseStorageUri: "s3://bucket/target/files",
433
- };
434
- const manifests = new Map([
435
- [
436
- targetBundle.manifestStorageUri,
437
- JSON.stringify({
438
- bundleId: targetBundle.id,
439
- assets: {
440
- "index.ios.bundle": {
441
- fileHash: "target-bundle-hash",
442
- },
443
- },
444
- }),
445
- ],
446
- ]);
447
- const getBundleById = vi.fn<DatabasePlugin<TestContext>["getBundleById"]>(
448
- async () => {
449
- throw new Error("unexpected provider bundle reread");
450
- },
451
- );
452
- const getUpdateInfo: NonNullable<
453
- DatabasePlugin<TestContext>["getUpdateInfo"]
454
- > = async (args, context) =>
455
- resolveUpdateInfoFromBundles({
456
- args,
457
- bundles: [targetBundle],
458
- context,
459
- });
460
- const plugin: DatabasePlugin<TestContext> = {
461
- name: "seeded-fast-path-plugin",
462
- async appendBundle() {},
463
- async commitBundle() {},
464
- async deleteBundle() {},
465
- getBundleById,
466
- async getBundles() {
467
- return {
468
- data: [targetBundle],
469
- pagination: {
470
- currentPage: 1,
471
- hasNextPage: false,
472
- hasPreviousPage: false,
473
- total: 1,
474
- totalPages: 1,
475
- },
476
- };
477
- },
478
- getUpdateInfo,
479
- async getChannels() {
480
- return ["production"];
481
- },
482
- async updateBundle() {},
483
- };
484
-
485
- const core = createPluginDatabaseCore(
486
- () => plugin,
487
- async (storageUri) => {
488
- if (!storageUri) return null;
489
- const url = new URL(storageUri);
490
- return `https://assets.example.com/${url.host}${url.pathname}`;
491
- },
492
- {
493
- readStorageText: async (storageUri) =>
494
- manifests.get(storageUri) ?? null,
495
- },
496
- );
497
- const context: TestContext = {
498
- env: {
499
- assetHost: "https://assets.example.com",
500
- },
501
- request: new Request("https://updates.example.com"),
502
- };
503
-
504
- const updateInfo = await core.api.getAppUpdateInfo(updateArgs, context);
505
-
506
- expect(updateInfo).toMatchObject({
507
- changedAssets: {
508
- "index.ios.bundle": {
509
- file: {
510
- compression: "br",
511
- url: "https://assets.example.com/bucket/target/files/index.ios.bundle.br",
512
- },
513
- fileHash: "target-bundle-hash",
514
- },
515
- },
516
- id: targetBundle.id,
517
- manifestFileHash: "sig:target-manifest",
518
- manifestUrl: "https://assets.example.com/bucket/target/manifest.json",
519
- });
520
- expect(getBundleById).not.toHaveBeenCalled();
521
- });
522
-
523
- it("does not reread providers for current bundles outside direct update lookup results", async () => {
524
- const targetBundle = {
525
- ...baseBundle,
526
- id: "00000000-0000-0000-0000-000000000003",
527
- fileHash: "hash-3",
528
- manifestStorageUri: "s3://bucket/target/manifest.json",
529
- manifestFileHash: "sig:target-manifest",
530
- assetBaseStorageUri: "s3://bucket/target/files",
531
- };
532
- const manifests = new Map([
533
- [
534
- targetBundle.manifestStorageUri,
535
- JSON.stringify({
536
- bundleId: targetBundle.id,
537
- assets: {
538
- "index.ios.bundle": {
539
- fileHash: "target-bundle-hash",
540
- },
541
- "image.png": {
542
- fileHash: "target-image-hash",
543
- },
544
- },
545
- }),
546
- ],
547
- ]);
548
- const getBundleById = vi.fn<DatabasePlugin<TestContext>["getBundleById"]>(
549
- async () => {
550
- throw new Error("unexpected provider current bundle reread");
551
- },
552
- );
553
- const getUpdateInfo: NonNullable<
554
- DatabasePlugin<TestContext>["getUpdateInfo"]
555
- > = async (args, context) =>
556
- resolveUpdateInfoFromBundles({
557
- args,
558
- bundles: [targetBundle],
559
- context,
560
- });
561
- const plugin: DatabasePlugin<TestContext> = {
562
- name: "seeded-current-miss-plugin",
563
- async appendBundle() {},
564
- async commitBundle() {},
565
- async deleteBundle() {},
566
- getBundleById,
567
- async getBundles() {
568
- throw new Error("unexpected provider scan");
569
- },
570
- getUpdateInfo,
571
- async getChannels() {
572
- return ["production"];
573
- },
574
- async updateBundle() {},
575
- };
576
-
577
- const core = createPluginDatabaseCore(
578
- () => plugin,
579
- async (storageUri) => {
580
- if (!storageUri) return null;
581
- const url = new URL(storageUri);
582
- return `https://assets.example.com/${url.host}${url.pathname}`;
583
- },
584
- {
585
- readStorageText: async (storageUri) =>
586
- manifests.get(storageUri) ?? null,
587
- },
588
- );
589
- const context: TestContext = {
590
- env: {
591
- assetHost: "https://assets.example.com",
592
- },
593
- request: new Request("https://updates.example.com"),
594
- };
595
-
596
- const updateInfo = await core.api.getAppUpdateInfo(
597
- {
598
- ...updateArgs,
599
- bundleId: "00000000-0000-0000-0000-0000000000aa",
600
- },
601
- context,
602
- );
603
-
604
- expect(updateInfo).toMatchObject({
605
- changedAssets: {
606
- "image.png": {
607
- file: {
608
- url: "https://assets.example.com/bucket/target/files/image.png",
609
- },
610
- fileHash: "target-image-hash",
611
- },
612
- "index.ios.bundle": {
613
- file: {
614
- compression: "br",
615
- url: "https://assets.example.com/bucket/target/files/index.ios.bundle.br",
616
- },
617
- fileHash: "target-bundle-hash",
618
- },
619
- },
620
- id: targetBundle.id,
621
- manifestFileHash: "sig:target-manifest",
622
- manifestUrl: "https://assets.example.com/bucket/target/manifest.json",
623
- });
624
- expect(getBundleById).not.toHaveBeenCalled();
625
- });
626
-
627
- it("resolves manifest changed assets from deterministic content-addressed storage", async () => {
628
- const currentBundle = {
629
- ...baseBundle,
630
- id: "00000000-0000-0000-0000-000000000001",
631
- manifestStorageUri: "r2://bucket/current/manifest.json",
632
- manifestFileHash: "sig:current-manifest",
633
- assetBaseStorageUri: "r2://bucket/assets",
634
- };
635
- const targetBundle = {
636
- ...baseBundle,
637
- id: "00000000-0000-0000-0000-000000000002",
638
- fileHash: "hash-2",
639
- manifestStorageUri: "r2://bucket/target/manifest.json",
640
- manifestFileHash: "sig:target-manifest",
641
- assetBaseStorageUri: "r2://bucket/assets",
642
- };
643
- const manifests = new Map([
644
- [
645
- currentBundle.manifestStorageUri,
646
- JSON.stringify({
647
- bundleId: currentBundle.id,
648
- assets: {
649
- "index.ios.bundle": {
650
- fileHash: "old-bundle-hash",
651
- },
652
- },
653
- }),
654
- ],
655
- [
656
- targetBundle.manifestStorageUri,
657
- JSON.stringify({
658
- bundleId: targetBundle.id,
659
- assets: {
660
- "index.ios.bundle": {
661
- fileHash: "new-bundle-hash",
662
- },
663
- },
664
- }),
665
- ],
666
- ]);
667
- const plugin: DatabasePlugin<TestContext> = {
668
- name: "content-addressed-manifest-plugin",
669
- async appendBundle() {},
670
- async commitBundle() {},
671
- async deleteBundle() {},
672
- async getBundleById(bundleId) {
673
- if (bundleId === currentBundle.id) return currentBundle;
674
- if (bundleId === targetBundle.id) return targetBundle;
675
- return null;
676
- },
677
- async getUpdateInfo() {
678
- return {
679
- fileHash: targetBundle.fileHash,
680
- id: targetBundle.id,
681
- message: targetBundle.message,
682
- shouldForceUpdate: targetBundle.shouldForceUpdate,
683
- status: "UPDATE",
684
- storageUri: targetBundle.storageUri,
685
- };
686
- },
687
- async getBundles() {
688
- return {
689
- data: [targetBundle],
690
- pagination: {
691
- currentPage: 1,
692
- hasNextPage: false,
693
- hasPreviousPage: false,
694
- total: 1,
695
- totalPages: 1,
696
- },
697
- };
698
- },
699
- async getChannels() {
700
- return ["production"];
701
- },
702
- async updateBundle() {},
703
- };
704
-
705
- const core = createPluginDatabaseCore(
706
- () => plugin,
707
- async (storageUri) => {
708
- if (!storageUri) return null;
709
- const url = new URL(storageUri);
710
- return `https://assets.example.com/${url.host}${url.pathname}`;
711
- },
712
- {
713
- readStorageText: async (storageUri) =>
714
- manifests.get(storageUri) ?? null,
715
- },
716
- );
717
-
718
- await expect(
719
- core.api.getAppUpdateInfo({
720
- ...updateArgs,
721
- bundleId: currentBundle.id,
722
- }),
723
- ).resolves.toMatchObject({
724
- changedAssets: {
725
- "index.ios.bundle": {
726
- file: {
727
- compression: "br",
728
- url: "https://assets.example.com/bucket/assets/sha256/ne/new-bundle-hash.br",
729
- },
730
- fileHash: "new-bundle-hash",
731
- },
732
- },
733
- });
734
- });
735
-
736
- it("falls back to archive metadata when manifest changed assets cannot be resolved", async () => {
737
- const currentBundle = {
738
- ...baseBundle,
739
- id: "00000000-0000-0000-0000-000000000001",
740
- manifestStorageUri: "r2://bucket/current/manifest.json",
741
- manifestFileHash: "sig:current-manifest",
742
- assetBaseStorageUri: "r2://bucket/current/files",
743
- };
744
- const targetBundle = {
745
- ...baseBundle,
746
- id: "00000000-0000-0000-0000-000000000002",
747
- fileHash: "hash-2",
748
- manifestStorageUri: "r2://bucket/target/manifest.json",
749
- manifestFileHash: "sig:target-manifest",
750
- assetBaseStorageUri: "r2://bucket/target/files",
751
- };
752
- const manifests = new Map([
753
- [
754
- currentBundle.manifestStorageUri,
755
- JSON.stringify({
756
- bundleId: currentBundle.id,
757
- assets: {
758
- "index.ios.bundle": {
759
- fileHash: "old-bundle-hash",
760
- },
761
- },
762
- }),
763
- ],
764
- [
765
- targetBundle.manifestStorageUri,
766
- JSON.stringify({
767
- bundleId: targetBundle.id,
768
- assets: {
769
- "index.ios.bundle": {
770
- fileHash: "new-bundle-hash",
771
- },
772
- },
773
- }),
774
- ],
775
- ]);
776
- const getUpdateInfo = vi.fn<
777
- NonNullable<DatabasePlugin<TestContext>["getUpdateInfo"]>
778
- >(async () => ({
779
- fileHash: targetBundle.fileHash,
780
- id: targetBundle.id,
781
- message: targetBundle.message,
782
- shouldForceUpdate: targetBundle.shouldForceUpdate,
783
- status: "UPDATE",
784
- storageUri: targetBundle.storageUri,
785
- }));
786
-
787
- const plugin: DatabasePlugin<TestContext> = {
788
- name: "manifest-unresolved-plugin",
789
- async appendBundle() {},
790
- async commitBundle() {},
791
- async deleteBundle() {},
792
- async getBundleById(bundleId) {
793
- if (bundleId === currentBundle.id) return currentBundle;
794
- if (bundleId === targetBundle.id) return targetBundle;
795
- return null;
796
- },
797
- getUpdateInfo,
798
- async getBundles() {
799
- return {
800
- data: [targetBundle],
801
- pagination: {
802
- currentPage: 1,
803
- hasNextPage: false,
804
- hasPreviousPage: false,
805
- total: 1,
806
- totalPages: 1,
807
- },
808
- };
809
- },
810
- async getChannels() {
811
- return ["production"];
812
- },
813
- async updateBundle() {},
814
- };
815
-
816
- const core = createPluginDatabaseCore(
817
- () => plugin,
818
- async (storageUri) => {
819
- if (!storageUri) return null;
820
- const url = new URL(storageUri);
821
- if (url.pathname.endsWith("/files/index.ios.bundle.br")) {
822
- return null;
823
- }
824
- return `https://assets.example.com/${url.host}${url.pathname}`;
825
- },
826
- {
827
- readStorageText: async (storageUri) =>
828
- manifests.get(storageUri) ?? null,
829
- },
830
- );
831
-
832
- const updateInfo = await core.api.getAppUpdateInfo({
833
- ...updateArgs,
834
- bundleId: currentBundle.id,
835
- });
836
-
837
- expect(updateInfo).toMatchObject({
838
- fileHash: "hash-2",
839
- fileUrl: "https://assets.example.com/bucket/bundle.zip",
840
- id: targetBundle.id,
841
- status: "UPDATE",
842
- });
843
- expect(updateInfo).not.toHaveProperty("changedAssets");
844
- expect(updateInfo).not.toHaveProperty("manifestFileHash");
845
- expect(updateInfo).not.toHaveProperty("manifestUrl");
846
- });
847
-
848
- it("propagates manifest storage read failures", async () => {
849
- const targetBundle = {
850
- ...baseBundle,
851
- id: "00000000-0000-0000-0000-000000000002",
852
- fileHash: "hash-2",
853
- manifestStorageUri: "r2://bucket/target/manifest.json",
854
- manifestFileHash: "sig:target-manifest",
855
- assetBaseStorageUri: "r2://bucket/target/files",
856
- };
857
- const storageError = new Error("storage read failed");
858
- const getUpdateInfo = vi.fn<
859
- NonNullable<DatabasePlugin<TestContext>["getUpdateInfo"]>
860
- >(async () => ({
861
- fileHash: targetBundle.fileHash,
862
- id: targetBundle.id,
863
- message: targetBundle.message,
864
- shouldForceUpdate: targetBundle.shouldForceUpdate,
865
- status: "UPDATE",
866
- storageUri: targetBundle.storageUri,
867
- }));
868
-
869
- const plugin: DatabasePlugin<TestContext> = {
870
- name: "manifest-error-plugin",
871
- async appendBundle() {},
872
- async commitBundle() {},
873
- async deleteBundle() {},
874
- async getBundleById(bundleId) {
875
- return bundleId === targetBundle.id ? targetBundle : null;
876
- },
877
- getUpdateInfo,
878
- async getBundles() {
879
- return {
880
- data: [targetBundle],
881
- pagination: {
882
- currentPage: 1,
883
- hasNextPage: false,
884
- hasPreviousPage: false,
885
- total: 1,
886
- totalPages: 1,
887
- },
888
- };
889
- },
890
- async getChannels() {
891
- return ["production"];
892
- },
893
- async updateBundle() {},
894
- };
895
-
896
- const core = createPluginDatabaseCore(
897
- () => plugin,
898
- async (storageUri) => {
899
- if (!storageUri) return null;
900
- const url = new URL(storageUri);
901
- return `https://assets.example.com/${url.host}${url.pathname}`;
902
- },
903
- {
904
- readStorageText: async () => {
905
- throw storageError;
906
- },
907
- },
908
- );
909
-
910
- await expect(core.api.getAppUpdateInfo(updateArgs)).rejects.toThrow(
911
- "storage read failed",
912
- );
913
- });
914
-
915
- it("does not fall back to scanning when plugin getUpdateInfo returns null", async () => {
916
- const getBundles = vi.fn<DatabasePlugin["getBundles"]>(async () => ({
917
- data: [baseBundle],
918
- pagination: {
919
- currentPage: 1,
920
- hasNextPage: false,
921
- hasPreviousPage: false,
922
- total: 1,
923
- totalPages: 1,
924
- },
925
- }));
926
- const getUpdateInfo = vi.fn<NonNullable<DatabasePlugin["getUpdateInfo"]>>(
927
- async () => null,
928
- );
929
-
930
- const plugin: DatabasePlugin = {
931
- name: "null-fast-path-plugin",
932
- async appendBundle() {},
933
- async commitBundle() {},
934
- async deleteBundle() {},
935
- async getBundleById() {
936
- return null;
937
- },
938
- getBundles,
939
- getUpdateInfo,
940
- async getChannels() {
941
- return ["production"];
942
- },
943
- async updateBundle() {},
944
- };
945
-
946
- const core = createPluginDatabaseCore(
947
- () => plugin,
948
- async () => null,
949
- );
950
-
951
- await expect(core.api.getUpdateInfo(updateArgs)).resolves.toBeNull();
952
- expect(getUpdateInfo).toHaveBeenCalledWith(updateArgs);
953
- expect(getBundles).not.toHaveBeenCalled();
954
- });
955
-
956
- it("falls back to scanning when plugin getUpdateInfo is absent", async () => {
957
- const latestBundle = {
958
- ...baseBundle,
959
- id: "00000000-0000-0000-0000-000000000002",
960
- };
961
- const getBundles = vi.fn<DatabasePlugin["getBundles"]>(async () => ({
962
- data: [latestBundle],
963
- pagination: {
964
- currentPage: 1,
965
- hasNextPage: false,
966
- hasPreviousPage: false,
967
- total: 1,
968
- totalPages: 1,
969
- },
970
- }));
971
-
972
- const plugin: DatabasePlugin = {
973
- name: "scan-plugin",
974
- async appendBundle() {},
975
- async commitBundle() {},
976
- async deleteBundle() {},
977
- async getBundleById() {
978
- return null;
979
- },
980
- getBundles,
981
- async getChannels() {
982
- return ["production"];
983
- },
984
- async updateBundle() {},
985
- };
986
-
987
- const core = createPluginDatabaseCore(
988
- () => plugin,
989
- async () => null,
990
- );
991
-
992
- await expect(core.api.getUpdateInfo(updateArgs)).resolves.toEqual({
993
- fileHash: latestBundle.fileHash,
994
- id: latestBundle.id,
995
- message: latestBundle.message,
996
- shouldForceUpdate: latestBundle.shouldForceUpdate,
997
- status: "UPDATE",
998
- storageUri: latestBundle.storageUri,
999
- });
1000
- expect(getBundles).toHaveBeenCalledOnce();
1001
- });
1002
-
1003
- it("rejects invalid bundles before appendBundle is called", async () => {
1004
- const appendBundle = vi.fn<DatabasePlugin["appendBundle"]>();
1005
- const commitBundle = vi.fn<DatabasePlugin["commitBundle"]>();
1006
-
1007
- const plugin: DatabasePlugin = {
1008
- name: "validation-plugin",
1009
- appendBundle,
1010
- commitBundle,
1011
- async deleteBundle() {},
1012
- async getBundleById() {
1013
- return null;
1014
- },
1015
- async getBundles() {
1016
- return {
1017
- data: [],
1018
- pagination: {
1019
- currentPage: 1,
1020
- hasNextPage: false,
1021
- hasPreviousPage: false,
1022
- total: 0,
1023
- totalPages: 1,
1024
- },
1025
- };
1026
- },
1027
- async getChannels() {
1028
- return ["production"];
1029
- },
1030
- async updateBundle() {},
1031
- };
1032
-
1033
- const core = createPluginDatabaseCore(
1034
- () => plugin,
1035
- async () => null,
1036
- );
1037
-
1038
- await expect(
1039
- core.api.insertBundle({
1040
- ...baseBundle,
1041
- targetAppVersion: null,
1042
- fingerprintHash: null,
1043
- }),
1044
- ).rejects.toThrow(
1045
- "Bundle must define either targetAppVersion or fingerprintHash.",
1046
- );
1047
- expect(appendBundle).not.toHaveBeenCalled();
1048
- expect(commitBundle).not.toHaveBeenCalled();
1049
- });
1050
-
1051
- it("rejects invalid updates before plugin.updateBundle is called", async () => {
1052
- const updateBundle = vi.fn<DatabasePlugin["updateBundle"]>();
1053
-
1054
- const plugin: DatabasePlugin = {
1055
- name: "update-validation-plugin",
1056
- async appendBundle() {},
1057
- async commitBundle() {},
1058
- async deleteBundle() {},
1059
- async getBundleById(bundleId) {
1060
- return bundleId === baseBundle.id ? baseBundle : null;
1061
- },
1062
- async getBundles() {
1063
- return {
1064
- data: [],
1065
- pagination: {
1066
- currentPage: 1,
1067
- hasNextPage: false,
1068
- hasPreviousPage: false,
1069
- total: 0,
1070
- totalPages: 1,
1071
- },
1072
- };
1073
- },
1074
- async getChannels() {
1075
- return ["production"];
1076
- },
1077
- updateBundle,
1078
- };
1079
-
1080
- const core = createPluginDatabaseCore(
1081
- () => plugin,
1082
- async () => null,
1083
- );
1084
-
1085
- await expect(
1086
- core.api.updateBundleById(baseBundle.id, {
1087
- targetAppVersion: null,
1088
- fingerprintHash: null,
1089
- }),
1090
- ).rejects.toThrow(
1091
- "Bundle must define either targetAppVersion or fingerprintHash.",
1092
- );
1093
- expect(updateBundle).not.toHaveBeenCalled();
1094
- });
1095
- });