@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
package/src/db/types.ts CHANGED
@@ -1,21 +1,23 @@
1
+ import type { ArtifactInfo, Bundle } from "@hot-updater/core";
1
2
  import type {
2
- AppUpdateAvailableInfo,
3
- Bundle,
4
- GetBundlesArgs,
5
- UpdateInfo,
6
- } from "@hot-updater/core";
7
- import type {
8
- DatabaseBundleQueryOptions,
9
- DatabasePlugin,
10
- HotUpdaterContext,
11
- RuntimeStoragePlugin,
3
+ ChannelDeleteInput,
4
+ ChannelDeleteResult,
5
+ ChannelInsertInput,
6
+ ChannelInsertResult,
7
+ ChannelRow,
8
+ DatabaseCommit,
9
+ DatabaseCommitResult,
10
+ DatabasePlugin as DatabasePluginContract,
11
+ ReleaseCatalogMutationPreflight,
12
+ ReleaseCatalogMutationResult,
13
+ ReleaseCatalogRebuildResult,
14
+ ReleaseCatalogRow,
15
+ ReleasePolicyPatch,
16
+ ReleaseRow,
12
17
  } from "@hot-updater/plugin-core";
13
18
 
14
19
  import type { PaginatedResult } from "../types";
15
20
 
16
- export type DatabasePluginFactory<TContext = unknown> =
17
- (() => DatabasePlugin<TContext>) & Partial<DatabaseAdapterCapabilities>;
18
-
19
21
  export const sqlProviders = [
20
22
  "sqlite",
21
23
  "cockroachdb",
@@ -45,6 +47,7 @@ export type MigrationOperation =
45
47
  columns: Record<string, { ormName: string; type: string }>;
46
48
  };
47
49
  }
50
+ | { type: "custom"; description: string }
48
51
  | { type: "custom"; sql: string }
49
52
  | { type: "custom"; key: string; value: unknown };
50
53
 
@@ -83,29 +86,66 @@ export interface DatabaseAdapterCapabilities {
83
86
  generateSchema?: SchemaGenerator;
84
87
  }
85
88
 
86
- export type DatabaseAdapterWithCapabilities<TContext = unknown> =
87
- DatabasePlugin<TContext> & DatabaseAdapterCapabilities;
89
+ export type DatabaseAdapterWithCapabilities = DatabasePluginContract &
90
+ DatabaseAdapterCapabilities;
88
91
 
89
- export type DatabaseAdapter<TContext = unknown> =
90
- | DatabaseAdapterWithCapabilities<TContext>
91
- | DatabasePlugin<TContext>
92
- | DatabasePluginFactory<TContext>;
92
+ export type DatabasePlugin = DatabaseAdapterWithCapabilities;
93
93
 
94
- export function isDatabasePluginFactory<TContext = unknown>(
95
- adapter: DatabaseAdapter<TContext>,
96
- ): adapter is DatabasePluginFactory<TContext> {
97
- return typeof adapter === "function";
98
- }
99
-
100
- export function isDatabasePlugin<TContext = unknown>(
101
- adapter: DatabaseAdapter<TContext>,
102
- ): adapter is DatabasePlugin<TContext> {
94
+ export function isDatabasePlugin(plugin: unknown): plugin is DatabasePlugin {
103
95
  return (
104
- typeof adapter === "object" &&
105
- adapter !== null &&
106
- "getBundleById" in adapter &&
107
- "getBundles" in adapter &&
108
- "getChannels" in adapter
96
+ typeof plugin === "object" &&
97
+ plugin !== null &&
98
+ "name" in plugin &&
99
+ typeof plugin.name === "string" &&
100
+ "models" in plugin &&
101
+ typeof plugin.models === "object" &&
102
+ plugin.models !== null &&
103
+ "bundles" in plugin.models &&
104
+ typeof plugin.models.bundles === "object" &&
105
+ plugin.models.bundles !== null &&
106
+ "findById" in plugin.models.bundles &&
107
+ typeof plugin.models.bundles.findById === "function" &&
108
+ "findMany" in plugin.models.bundles &&
109
+ typeof plugin.models.bundles.findMany === "function" &&
110
+ "count" in plugin.models.bundles &&
111
+ typeof plugin.models.bundles.count === "function" &&
112
+ "bundlePatches" in plugin.models &&
113
+ typeof plugin.models.bundlePatches === "object" &&
114
+ plugin.models.bundlePatches !== null &&
115
+ "findByBundleIds" in plugin.models.bundlePatches &&
116
+ typeof plugin.models.bundlePatches.findByBundleIds === "function" &&
117
+ "channels" in plugin.models &&
118
+ typeof plugin.models.channels === "object" &&
119
+ plugin.models.channels !== null &&
120
+ "insert" in plugin.models.channels &&
121
+ typeof plugin.models.channels.insert === "function" &&
122
+ "list" in plugin.models.channels &&
123
+ typeof plugin.models.channels.list === "function" &&
124
+ "delete" in plugin.models.channels &&
125
+ typeof plugin.models.channels.delete === "function" &&
126
+ "analytics" in plugin.models &&
127
+ typeof plugin.models.analytics === "object" &&
128
+ plugin.models.analytics !== null &&
129
+ "append" in plugin.models.analytics &&
130
+ typeof plugin.models.analytics.append === "function" &&
131
+ "scan" in plugin.models.analytics &&
132
+ typeof plugin.models.analytics.scan === "function" &&
133
+ "apiKeys" in plugin.models &&
134
+ typeof plugin.models.apiKeys === "object" &&
135
+ plugin.models.apiKeys !== null &&
136
+ "create" in plugin.models.apiKeys &&
137
+ typeof plugin.models.apiKeys.create === "function" &&
138
+ "findByHash" in plugin.models.apiKeys &&
139
+ typeof plugin.models.apiKeys.findByHash === "function" &&
140
+ "list" in plugin.models.apiKeys &&
141
+ typeof plugin.models.apiKeys.list === "function" &&
142
+ "revoke" in plugin.models.apiKeys &&
143
+ typeof plugin.models.apiKeys.revoke === "function" &&
144
+ "commit" in plugin &&
145
+ typeof plugin.commit === "function" &&
146
+ (!("dispose" in plugin) ||
147
+ plugin.dispose === undefined ||
148
+ typeof plugin.dispose === "function")
109
149
  );
110
150
  }
111
151
 
@@ -121,38 +161,62 @@ export function getSQLProvider(
121
161
  : undefined;
122
162
  }
123
163
 
124
- export interface DatabaseAPI<TContext = unknown> {
125
- getBundleById(
126
- id: string,
127
- context?: HotUpdaterContext<TContext>,
128
- ): Promise<Bundle | null>;
129
- getUpdateInfo(
130
- args: GetBundlesArgs,
131
- context?: HotUpdaterContext<TContext>,
132
- ): Promise<UpdateInfo | null>;
133
- getAppUpdateInfo(
134
- args: GetBundlesArgs,
135
- context?: HotUpdaterContext<TContext>,
136
- ): Promise<AppUpdateAvailableInfo | null>;
137
- getChannels(context?: HotUpdaterContext<TContext>): Promise<string[]>;
164
+ export interface DatabaseAPI {
165
+ getBundleById(id: string): Promise<Bundle | null>;
166
+ getReleaseCatalog(
167
+ input: import("./releaseCatalog").ReleaseCatalogRequest,
168
+ ): Promise<import("@hot-updater/core").ReleaseCatalog | null>;
169
+ getArtifactInfo(
170
+ targetBundleId: string,
171
+ currentBundleId: string,
172
+ ): Promise<ArtifactInfo | null>;
173
+ getReleaseById(id: string): Promise<ReleaseRow | null>;
174
+ getReleasesByScope(input: {
175
+ readonly scopeKey: string;
176
+ readonly afterReleaseId?: string;
177
+ readonly limit: number;
178
+ }): Promise<readonly ReleaseRow[]>;
179
+ getReleases(input: {
180
+ readonly afterReleaseId?: string;
181
+ readonly beforeReleaseId?: string;
182
+ readonly bundleId?: string;
183
+ readonly channelId?: string;
184
+ readonly enabled?: boolean;
185
+ readonly platform?: "ios" | "android";
186
+ readonly targetAppVersion?: string;
187
+ readonly limit: number;
188
+ }): Promise<readonly ReleaseRow[]>;
189
+ getReleaseCatalogByScopeKey(
190
+ scopeKey: string,
191
+ ): Promise<ReleaseCatalogRow | null>;
192
+ getReleaseCatalogs(input: {
193
+ readonly afterScopeKey?: string;
194
+ readonly limit: number;
195
+ }): Promise<readonly ReleaseCatalogRow[]>;
196
+ updateReleasePolicy(input: {
197
+ readonly expectedRevision?: number;
198
+ readonly patch: ReleasePolicyPatch;
199
+ readonly releaseId: string;
200
+ }): Promise<ReleaseCatalogMutationResult>;
201
+ preflightReleasePolicy(input: {
202
+ readonly expectedRevision?: number;
203
+ readonly patch: ReleasePolicyPatch;
204
+ readonly releaseId: string;
205
+ }): Promise<ReleaseCatalogMutationPreflight>;
206
+ deleteRelease(input: {
207
+ readonly expectedRevision?: number;
208
+ readonly releaseId: string;
209
+ }): Promise<ReleaseCatalogMutationResult>;
210
+ rebuildReleaseCatalog(scopeKey: string): Promise<ReleaseCatalogRebuildResult>;
211
+ commitDatabase(input: DatabaseCommit): Promise<DatabaseCommitResult>;
212
+ getChannels(): Promise<readonly ChannelRow[]>;
213
+ insertChannel(input: ChannelInsertInput): Promise<ChannelInsertResult>;
214
+ deleteChannel(input: ChannelDeleteInput): Promise<ChannelDeleteResult>;
138
215
  getBundles(
139
- options: DatabaseBundleQueryOptions,
140
- context?: HotUpdaterContext<TContext>,
216
+ options: import("@hot-updater/plugin-core").DatabaseBundleQueryOptions,
141
217
  ): Promise<PaginatedResult>;
142
- insertBundle(
143
- bundle: Bundle,
144
- context?: HotUpdaterContext<TContext>,
145
- ): Promise<void>;
146
- updateBundleById(
147
- bundleId: string,
148
- newBundle: Partial<Bundle>,
149
- context?: HotUpdaterContext<TContext>,
150
- ): Promise<void>;
151
- deleteBundleById(
152
- bundleId: string,
153
- context?: HotUpdaterContext<TContext>,
154
- ): Promise<void>;
218
+ insertBundle(bundle: Bundle): Promise<void>;
219
+ insertBundles(bundles: readonly Bundle[]): Promise<void>;
220
+ updateBundleById(bundleId: string, newBundle: Partial<Bundle>): Promise<void>;
221
+ deleteBundleById(bundleId: string): Promise<void>;
155
222
  }
156
-
157
- export type StoragePluginFactory<TContext = unknown> =
158
- () => RuntimeStoragePlugin<TContext>;
@@ -0,0 +1,359 @@
1
+ import { NIL_UUID, type Bundle } from "@hot-updater/core";
2
+ import { createDatabaseClient } from "@hot-updater/plugin-core";
3
+ import { describe, expect, it, vi } from "vitest";
4
+
5
+ import { createInMemoryDatabasePlugin } from "../../../test-utils/test/inMemoryDatabasePlugin";
6
+ import { createArtifactResolver } from "./releaseCatalog";
7
+ import { resolveManifestArtifacts } from "./updateArtifacts";
8
+
9
+ type TestManifestAsset = {
10
+ downloadByteSize?: unknown;
11
+ downloadFileHash?: unknown;
12
+ fileHash: string;
13
+ };
14
+
15
+ const CURRENT_BUNDLE_ID = "00000000-0000-0000-0000-000000000101";
16
+ const TARGET_BUNDLE_ID = "00000000-0000-0000-0000-000000000102";
17
+ const CURRENT_MANIFEST_URI = `s3://test-bucket/bundles/${CURRENT_BUNDLE_ID}/manifest.json`;
18
+ const TARGET_MANIFEST_URI = `s3://test-bucket/bundles/${TARGET_BUNDLE_ID}/manifest.json`;
19
+ const PATCH_STORAGE_URI = `s3://test-bucket/bundles/${TARGET_BUNDLE_ID}/patches/${CURRENT_BUNDLE_ID}/patch.bsdiff`;
20
+ const VALID_DOWNLOAD_FILE_HASH = "a".repeat(64);
21
+
22
+ const getUtf8ByteSize = (value: string) =>
23
+ new TextEncoder().encode(value).byteLength;
24
+
25
+ const createBundle = (
26
+ id: string,
27
+ archiveByteSize: number,
28
+ overrides: Partial<Bundle> = {},
29
+ ): Bundle => ({
30
+ archiveByteSize,
31
+ assetBaseStorageUri: "s3://test-bucket/assets",
32
+ fileHash: `${id}-archive-hash`,
33
+ gitCommitHash: null,
34
+ id,
35
+ manifestFileHash: `${id}-manifest-hash`,
36
+ manifestStorageUri:
37
+ id === CURRENT_BUNDLE_ID ? CURRENT_MANIFEST_URI : TARGET_MANIFEST_URI,
38
+ platform: "ios",
39
+ storageUri: `s3://test-bucket/bundles/${id}/bundle.zip`,
40
+ ...overrides,
41
+ });
42
+
43
+ async function runScenario({
44
+ archiveByteSize,
45
+ archiveUrlUsable = true,
46
+ assetPath = "index.ios.bundle",
47
+ patchByteSize,
48
+ resolveUrl,
49
+ targetAsset,
50
+ targetManifestSuffix = "",
51
+ }: {
52
+ archiveByteSize: number | ((manifestByteSize: number) => number);
53
+ archiveUrlUsable?: boolean;
54
+ assetPath?: string;
55
+ patchByteSize?: unknown;
56
+ resolveUrl?: (storageUri: string) => Promise<string | null>;
57
+ targetAsset: TestManifestAsset;
58
+ targetManifestSuffix?: string;
59
+ }) {
60
+ const currentManifestText = JSON.stringify({
61
+ assets: {
62
+ [assetPath]: {
63
+ fileHash: "current-file-hash",
64
+ },
65
+ },
66
+ bundleId: CURRENT_BUNDLE_ID,
67
+ });
68
+ const targetManifestText = `${JSON.stringify({
69
+ assets: {
70
+ [assetPath]: targetAsset,
71
+ },
72
+ bundleId: TARGET_BUNDLE_ID,
73
+ })}${targetManifestSuffix}`;
74
+ const manifestByteSize = getUtf8ByteSize(targetManifestText);
75
+ const resolvedArchiveByteSize =
76
+ typeof archiveByteSize === "function"
77
+ ? archiveByteSize(manifestByteSize)
78
+ : archiveByteSize;
79
+ const hasPatch = patchByteSize !== undefined;
80
+ const currentBundle = createBundle(CURRENT_BUNDLE_ID, 1_024);
81
+ const targetBundle = createBundle(
82
+ TARGET_BUNDLE_ID,
83
+ resolvedArchiveByteSize,
84
+ hasPatch
85
+ ? {
86
+ patches: [
87
+ {
88
+ baseBundleId: CURRENT_BUNDLE_ID,
89
+ baseFileHash: "current-file-hash",
90
+ byteSize: patchByteSize as number,
91
+ patchFileHash: "patch-file-hash",
92
+ patchStorageUri: PATCH_STORAGE_URI,
93
+ },
94
+ ],
95
+ }
96
+ : {},
97
+ );
98
+ const storageTexts = new Map([
99
+ [CURRENT_MANIFEST_URI, currentManifestText],
100
+ [TARGET_MANIFEST_URI, targetManifestText],
101
+ ]);
102
+ const resolveFileUrl = vi.fn(async (storageUri: string | null) => {
103
+ if (!storageUri) return null;
104
+ if (resolveUrl) return resolveUrl(storageUri);
105
+ return `https://download.example.com/${encodeURIComponent(storageUri)}`;
106
+ });
107
+
108
+ const result = await resolveManifestArtifacts({
109
+ archiveUrlUsable,
110
+ currentBundle,
111
+ readStorageText: async (storageUri) => storageTexts.get(storageUri) ?? null,
112
+ resolveFileUrl,
113
+ targetBundle,
114
+ });
115
+
116
+ return {
117
+ manifestByteSize,
118
+ resolveFileUrl,
119
+ result,
120
+ };
121
+ }
122
+
123
+ describe("resolveManifestArtifacts", () => {
124
+ it("uses raw UTF-8 manifest bytes and selects archive at equal size", async () => {
125
+ const { resolveFileUrl, result } = await runScenario({
126
+ archiveByteSize: (manifestByteSize) => manifestByteSize + 7,
127
+ assetPath: "assets/한글.png",
128
+ targetAsset: {
129
+ downloadByteSize: 7,
130
+ fileHash: "target-file-hash",
131
+ },
132
+ targetManifestSuffix: "\n",
133
+ });
134
+
135
+ expect(result).toBeNull();
136
+ expect(resolveFileUrl).not.toHaveBeenCalled();
137
+ });
138
+
139
+ it("accepts a valid download hash and size pair on a raw asset", async () => {
140
+ const { resolveFileUrl, result } = await runScenario({
141
+ archiveByteSize: (manifestByteSize) => manifestByteSize + 7,
142
+ assetPath: "assets/logo.png",
143
+ targetAsset: {
144
+ downloadByteSize: 7,
145
+ downloadFileHash: VALID_DOWNLOAD_FILE_HASH,
146
+ fileHash: "target-file-hash",
147
+ },
148
+ });
149
+
150
+ expect(result).toBeNull();
151
+ expect(resolveFileUrl).not.toHaveBeenCalled();
152
+ });
153
+
154
+ it("treats an invalid present hash on a raw asset as unknown cost", async () => {
155
+ const { resolveFileUrl, result } = await runScenario({
156
+ archiveByteSize: 1,
157
+ assetPath: "assets/logo.png",
158
+ targetAsset: {
159
+ downloadByteSize: 100,
160
+ downloadFileHash: "INVALID",
161
+ fileHash: "target-file-hash",
162
+ },
163
+ });
164
+
165
+ expect(result).not.toBeNull();
166
+ expect(resolveFileUrl).toHaveBeenCalledWith(
167
+ "s3://test-bucket/assets/sha256/ta/target-file-hash.png",
168
+ );
169
+ });
170
+
171
+ it.each([
172
+ ["missing", undefined],
173
+ ["invalid", "INVALID"],
174
+ ])(
175
+ "treats a Brotli asset as unknown cost when its download hash is %s",
176
+ async (_label, downloadFileHash) => {
177
+ const { resolveFileUrl, result } = await runScenario({
178
+ archiveByteSize: 1,
179
+ targetAsset: {
180
+ downloadByteSize: 100,
181
+ downloadFileHash,
182
+ fileHash: "target-file-hash",
183
+ },
184
+ });
185
+
186
+ expect(result).not.toBeNull();
187
+ expect(resolveFileUrl).toHaveBeenCalledWith(
188
+ "s3://test-bucket/assets/sha256/ta/target-file-hash.br",
189
+ );
190
+ },
191
+ );
192
+
193
+ it.each([
194
+ ["keeps", 9, true],
195
+ ["omits", 10, false],
196
+ ])(
197
+ "%s a %i-byte patch when the file is 10 bytes",
198
+ async (_action, patchByteSize, expectsPatch) => {
199
+ const { resolveFileUrl, result } = await runScenario({
200
+ archiveByteSize: (manifestByteSize) => manifestByteSize + 1_000,
201
+ patchByteSize,
202
+ targetAsset: {
203
+ downloadByteSize: 10,
204
+ downloadFileHash: VALID_DOWNLOAD_FILE_HASH,
205
+ fileHash: "target-file-hash",
206
+ },
207
+ });
208
+
209
+ expect(result?.changedAssets?.["index.ios.bundle"]?.patch).toEqual(
210
+ expectsPatch
211
+ ? expect.objectContaining({ patchUrl: expect.any(String) })
212
+ : undefined,
213
+ );
214
+ expect(resolveFileUrl).toHaveBeenCalledWith(
215
+ `s3://test-bucket/assets/sha256/aa/${VALID_DOWNLOAD_FILE_HASH}.br`,
216
+ );
217
+ expect(resolveFileUrl).toHaveBeenCalledWith(PATCH_STORAGE_URI);
218
+ },
219
+ );
220
+
221
+ it("keeps a larger patch when the file and archive URLs are unavailable", async () => {
222
+ const fileStorageUri = `s3://test-bucket/assets/sha256/aa/${VALID_DOWNLOAD_FILE_HASH}.br`;
223
+ const { result } = await runScenario({
224
+ archiveByteSize: 1,
225
+ archiveUrlUsable: false,
226
+ patchByteSize: 100,
227
+ resolveUrl: async (storageUri) =>
228
+ storageUri === fileStorageUri
229
+ ? null
230
+ : `https://download.example.com/${encodeURIComponent(storageUri)}`,
231
+ targetAsset: {
232
+ downloadByteSize: 50,
233
+ downloadFileHash: VALID_DOWNLOAD_FILE_HASH,
234
+ fileHash: "target-file-hash",
235
+ },
236
+ });
237
+
238
+ expect(result?.changedAssets?.["index.ios.bundle"]).toEqual({
239
+ fileHash: "target-file-hash",
240
+ patch: expect.objectContaining({
241
+ patchUrl: expect.any(String),
242
+ }),
243
+ });
244
+ });
245
+
246
+ it("recalculates from the file when an unknown-size patch URL is unavailable", async () => {
247
+ const { resolveFileUrl, result } = await runScenario({
248
+ archiveByteSize: (manifestByteSize) => manifestByteSize + 50,
249
+ patchByteSize: Number.NaN,
250
+ resolveUrl: async (storageUri) =>
251
+ storageUri === PATCH_STORAGE_URI
252
+ ? null
253
+ : `https://download.example.com/${encodeURIComponent(storageUri)}`,
254
+ targetAsset: {
255
+ downloadByteSize: 100,
256
+ downloadFileHash: VALID_DOWNLOAD_FILE_HASH,
257
+ fileHash: "target-file-hash",
258
+ },
259
+ });
260
+
261
+ expect(result).toBeNull();
262
+ expect(resolveFileUrl).toHaveBeenCalledWith(PATCH_STORAGE_URI);
263
+ expect(resolveFileUrl).toHaveBeenCalledWith(
264
+ `s3://test-bucket/assets/sha256/aa/${VALID_DOWNLOAD_FILE_HASH}.br`,
265
+ );
266
+ });
267
+
268
+ it("recalculates from a known patch when the file fallback cost is unknown", async () => {
269
+ const { result } = await runScenario({
270
+ archiveByteSize: (manifestByteSize) => manifestByteSize + 50,
271
+ patchByteSize: 100,
272
+ targetAsset: {
273
+ downloadByteSize: 100,
274
+ fileHash: "target-file-hash",
275
+ },
276
+ });
277
+
278
+ expect(result).toBeNull();
279
+ });
280
+
281
+ it("keeps the manifest path when safe addition overflows", async () => {
282
+ const { result } = await runScenario({
283
+ archiveByteSize: 1,
284
+ assetPath: "assets/logo.png",
285
+ targetAsset: {
286
+ downloadByteSize: Number.MAX_SAFE_INTEGER,
287
+ fileHash: "target-file-hash",
288
+ },
289
+ });
290
+
291
+ expect(result).not.toBeNull();
292
+ });
293
+
294
+ it("keeps the manifest path when a byte size is invalid", async () => {
295
+ const { result } = await runScenario({
296
+ archiveByteSize: 1,
297
+ assetPath: "assets/logo.png",
298
+ targetAsset: {
299
+ downloadByteSize: -1,
300
+ fileHash: "target-file-hash",
301
+ },
302
+ });
303
+
304
+ expect(result).not.toBeNull();
305
+ });
306
+
307
+ it("does not select archive when its URL is unavailable", async () => {
308
+ const { result } = await runScenario({
309
+ archiveByteSize: 1,
310
+ archiveUrlUsable: false,
311
+ assetPath: "assets/logo.png",
312
+ targetAsset: {
313
+ downloadByteSize: 100,
314
+ fileHash: "target-file-hash",
315
+ },
316
+ });
317
+
318
+ expect(result).not.toBeNull();
319
+ });
320
+ });
321
+
322
+ describe("createArtifactResolver", () => {
323
+ it("returns usable manifest artifacts when the archive URL is null", async () => {
324
+ const databasePlugin = createInMemoryDatabasePlugin();
325
+ const database = createDatabaseClient(databasePlugin);
326
+ const targetManifestText = JSON.stringify({
327
+ assets: {
328
+ "assets/logo.png": {
329
+ downloadByteSize: 100,
330
+ fileHash: "target-file-hash",
331
+ },
332
+ },
333
+ bundleId: TARGET_BUNDLE_ID,
334
+ });
335
+ const targetBundle = createBundle(TARGET_BUNDLE_ID, 1);
336
+ await database.insertBundle(targetBundle);
337
+
338
+ const resolver = createArtifactResolver({
339
+ database: databasePlugin,
340
+ readStorageText: async (storageUri) =>
341
+ storageUri === TARGET_MANIFEST_URI ? targetManifestText : null,
342
+ resolveFileUrl: async (storageUri) =>
343
+ storageUri === targetBundle.storageUri
344
+ ? null
345
+ : `https://download.example.com/${encodeURIComponent(storageUri ?? "")}`,
346
+ });
347
+
348
+ await expect(resolver(TARGET_BUNDLE_ID, NIL_UUID)).resolves.toMatchObject({
349
+ changedAssets: {
350
+ "assets/logo.png": {
351
+ file: { url: expect.any(String) },
352
+ fileHash: "target-file-hash",
353
+ },
354
+ },
355
+ fileUrl: null,
356
+ manifestUrl: expect.any(String),
357
+ });
358
+ });
359
+ });