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

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/apiKeys.cjs +139 -0
  44. package/dist/apiKeys.d.cts +50 -0
  45. package/dist/apiKeys.d.mts +50 -0
  46. package/dist/apiKeys.mjs +133 -0
  47. package/dist/createHotUpdaterCore.cjs +48 -26
  48. package/dist/createHotUpdaterCore.d.cts +49 -17
  49. package/dist/createHotUpdaterCore.d.mts +49 -17
  50. package/dist/createHotUpdaterCore.mjs +51 -29
  51. package/dist/db/createBundleDiff.cjs +45 -68
  52. package/dist/db/createBundleDiff.d.cts +3 -3
  53. package/dist/db/createBundleDiff.d.mts +3 -3
  54. package/dist/db/createBundleDiff.mjs +47 -68
  55. package/dist/db/databasePluginCore.cjs +138 -0
  56. package/dist/db/databasePluginCore.mjs +137 -0
  57. package/dist/db/fixedMigrator.cjs +36 -93
  58. package/dist/db/fixedMigrator.mjs +36 -93
  59. package/dist/db/fixedMigratorMongo.cjs +319 -0
  60. package/dist/db/fixedMigratorMongo.mjs +319 -0
  61. package/dist/db/fixedMigratorShared.cjs +23 -0
  62. package/dist/db/fixedMigratorShared.mjs +18 -0
  63. package/dist/db/mongoMigrationExecution.cjs +11 -0
  64. package/dist/db/mongoMigrationExecution.mjs +11 -0
  65. package/dist/db/releaseCatalog.cjs +66 -0
  66. package/dist/db/releaseCatalog.d.cts +17 -0
  67. package/dist/db/releaseCatalog.d.mts +17 -0
  68. package/dist/db/releaseCatalog.mjs +64 -0
  69. package/dist/db/schema/mongodb.cjs +14 -6
  70. package/dist/db/schema/mongodb.mjs +14 -6
  71. package/dist/db/schema/registry.cjs +0 -2
  72. package/dist/db/schema/registry.mjs +1 -2
  73. package/dist/db/schema/sql.cjs +31 -14
  74. package/dist/db/schema/sql.mjs +32 -13
  75. package/dist/db/schema/sqlOperations.cjs +4 -4
  76. package/dist/db/schema/sqlOperations.mjs +5 -5
  77. package/dist/db/schemaGenerators.cjs +52 -18
  78. package/dist/db/schemaGenerators.mjs +52 -18
  79. package/dist/db/schemaReadiness.cjs +4 -3
  80. package/dist/db/schemaReadiness.mjs +4 -3
  81. package/dist/db/sqlMigrationExecution.cjs +67 -0
  82. package/dist/db/sqlMigrationExecution.mjs +66 -0
  83. package/dist/db/types.cjs +2 -6
  84. package/dist/db/types.d.cts +60 -16
  85. package/dist/db/types.d.mts +60 -16
  86. package/dist/db/types.mjs +3 -6
  87. package/dist/db/updateArtifacts.cjs +114 -60
  88. package/dist/db/updateArtifacts.mjs +117 -62
  89. package/dist/handler.cjs +108 -282
  90. package/dist/handler.d.cts +3 -46
  91. package/dist/handler.d.mts +3 -46
  92. package/dist/handler.mjs +107 -281
  93. package/dist/handlerBundleRoutes.cjs +155 -0
  94. package/dist/handlerBundleRoutes.mjs +155 -0
  95. package/dist/handlerErrors.cjs +9 -0
  96. package/dist/handlerErrors.mjs +9 -0
  97. package/dist/handlerParameters.cjs +39 -0
  98. package/dist/handlerParameters.mjs +34 -0
  99. package/dist/handlerReleaseCatalogRoutes.cjs +111 -0
  100. package/dist/handlerReleaseCatalogRoutes.mjs +110 -0
  101. package/dist/handlerReleaseManagementRoutes.cjs +125 -0
  102. package/dist/handlerReleaseManagementRoutes.mjs +124 -0
  103. package/dist/handlerTypes.d.cts +63 -0
  104. package/dist/handlerTypes.d.mts +63 -0
  105. package/dist/handlerVersionRoutes.cjs +10 -0
  106. package/dist/handlerVersionRoutes.d.cts +4 -0
  107. package/dist/handlerVersionRoutes.d.mts +4 -0
  108. package/dist/handlerVersionRoutes.mjs +9 -0
  109. package/dist/index.cjs +10 -1
  110. package/dist/index.d.cts +9 -3
  111. package/dist/index.d.mts +9 -3
  112. package/dist/index.mjs +5 -2
  113. package/dist/insights/bounded/activeOverview.cjs +87 -0
  114. package/dist/insights/bounded/activeOverview.mjs +85 -0
  115. package/dist/insights/bounded/installationSearch.cjs +42 -0
  116. package/dist/insights/bounded/installationSearch.mjs +42 -0
  117. package/dist/insights/bounded/persistence.cjs +39 -0
  118. package/dist/insights/bounded/persistence.mjs +38 -0
  119. package/dist/insights/bounded/provider.cjs +170 -0
  120. package/dist/insights/bounded/provider.d.cts +7 -0
  121. package/dist/insights/bounded/provider.d.mts +7 -0
  122. package/dist/insights/bounded/provider.mjs +170 -0
  123. package/dist/insights/bounded/scan.cjs +144 -0
  124. package/dist/insights/bounded/scan.mjs +138 -0
  125. package/dist/insights/domain.d.cts +121 -0
  126. package/dist/insights/domain.d.mts +121 -0
  127. package/dist/insights/errors.cjs +22 -0
  128. package/dist/insights/errors.mjs +20 -0
  129. package/dist/insights/eventInput.cjs +111 -0
  130. package/dist/insights/eventInput.mjs +111 -0
  131. package/dist/insights/persistence.d.cts +6 -0
  132. package/dist/insights/persistence.d.mts +6 -0
  133. package/dist/insights/queryInput.cjs +58 -0
  134. package/dist/insights/queryInput.mjs +55 -0
  135. package/dist/insights/routes.cjs +63 -0
  136. package/dist/insights/routes.mjs +61 -0
  137. package/dist/insights/types.d.cts +17 -0
  138. package/dist/insights/types.d.mts +17 -0
  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/apiKeys.integration.spec.ts +192 -0
  188. package/src/apiKeys.spec.ts +162 -0
  189. package/src/apiKeys.ts +220 -0
  190. package/src/createHotUpdaterCore.ts +164 -100
  191. package/src/db/bundleRows.spec.ts +164 -0
  192. package/src/db/bundleRows.ts +10 -140
  193. package/src/db/createBundleDiff.spec.ts +172 -127
  194. package/src/db/createBundleDiff.ts +91 -110
  195. package/src/db/databasePluginCore.spec.ts +62 -0
  196. package/src/db/databasePluginCore.testFixtures.ts +76 -0
  197. package/src/db/databasePluginCore.ts +185 -0
  198. package/src/db/databasePluginUpdateCheck.bench.ts +107 -0
  199. package/src/db/fixedMigrator.spec.ts +184 -42
  200. package/src/db/fixedMigrator.ts +64 -205
  201. package/src/db/fixedMigratorMongo.spec.ts +192 -0
  202. package/src/db/fixedMigratorMongo.ts +425 -0
  203. package/src/db/fixedMigratorShared.ts +36 -0
  204. package/src/db/hotUpdaterSchema.ts +0 -1
  205. package/src/db/index.spec.ts +333 -1025
  206. package/src/db/mongoMigrationExecution.ts +23 -0
  207. package/src/db/node-sqlite.d.ts +25 -0
  208. package/src/db/releaseCatalog.ts +148 -0
  209. package/src/db/schema/mongodb.ts +20 -6
  210. package/src/db/schema/sql.ts +52 -11
  211. package/src/db/schema/sqlOperations.ts +5 -4
  212. package/src/db/schemaGenerators.ts +107 -23
  213. package/src/db/schemaReadiness.ts +2 -1
  214. package/src/db/sqlMigrationExecution.spec.ts +65 -0
  215. package/src/db/sqlMigrationExecution.ts +108 -0
  216. package/src/db/types.spec.ts +55 -0
  217. package/src/db/types.ts +129 -65
  218. package/src/db/updateArtifacts.spec.ts +359 -0
  219. package/src/db/updateArtifacts.ts +290 -126
  220. package/src/handler-standalone.integration.spec.ts +401 -364
  221. package/src/handler.spec.ts +137 -582
  222. package/src/handler.testFixtures.ts +30 -0
  223. package/src/handler.ts +218 -616
  224. package/src/handlerAuth.integration.spec.ts +33 -0
  225. package/src/handlerBundleRoutes.ts +259 -0
  226. package/src/handlerErrors.ts +7 -0
  227. package/src/handlerManagement.spec.ts +262 -0
  228. package/src/handlerParameters.ts +105 -0
  229. package/src/handlerReleaseCatalogRoutes.ts +154 -0
  230. package/src/handlerReleaseManagementRoutes.ts +213 -0
  231. package/src/handlerTypes.ts +96 -0
  232. package/src/handlerVersionRoutes.ts +12 -0
  233. package/src/index.ts +22 -2
  234. package/src/insights/bounded/activeOverview.spec.ts +189 -0
  235. package/src/insights/bounded/activeOverview.ts +148 -0
  236. package/src/insights/bounded/installationSearch.spec.ts +130 -0
  237. package/src/insights/bounded/installationSearch.ts +69 -0
  238. package/src/insights/bounded/persistence.ts +43 -0
  239. package/src/insights/bounded/provider.spec.ts +219 -0
  240. package/src/insights/bounded/provider.ts +229 -0
  241. package/src/insights/bounded/scan.ts +235 -0
  242. package/src/insights/domain.ts +139 -0
  243. package/src/insights/errors.ts +18 -0
  244. package/src/insights/eventInput.ts +168 -0
  245. package/src/insights/insights.integration.spec.ts +144 -0
  246. package/src/insights/persistence.ts +10 -0
  247. package/src/insights/queryInput.ts +102 -0
  248. package/src/insights/routes.ts +123 -0
  249. package/src/insights/types.ts +44 -0
  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
@@ -0,0 +1,128 @@
1
+ import type {
2
+ DatabaseModel,
3
+ DatabaseWhere,
4
+ } from "@hot-updater/plugin-core/internal";
5
+ import { sql, type RawBuilder } from "kysely";
6
+
7
+ import type { ORMSQLProvider } from "../db/types";
8
+ import { escapeGlobPattern, escapeLikePattern } from "./databasePluginUtils";
9
+
10
+ class InvalidDatabasePredicateError extends Error {
11
+ readonly name = "InvalidDatabasePredicateError";
12
+ }
13
+
14
+ const stringPredicate = (
15
+ provider: ORMSQLProvider,
16
+ field: string,
17
+ operator: "contains" | "ends_with" | "starts_with",
18
+ value: string,
19
+ insensitive: boolean,
20
+ ): RawBuilder<boolean> => {
21
+ const column = sql.ref(field);
22
+ if (provider === "sqlite" && !insensitive) {
23
+ const literal = escapeGlobPattern(value);
24
+ const pattern =
25
+ operator === "contains"
26
+ ? `*${literal}*`
27
+ : operator === "starts_with"
28
+ ? `${literal}*`
29
+ : `*${literal}`;
30
+ return sql<boolean>`${column} glob ${pattern}`;
31
+ }
32
+
33
+ const literal = escapeLikePattern(value);
34
+ const pattern =
35
+ operator === "contains"
36
+ ? `%${literal}%`
37
+ : operator === "starts_with"
38
+ ? `${literal}%`
39
+ : `%${literal}`;
40
+ if (insensitive) {
41
+ return sql<boolean>`lower(${column}) like lower(${pattern}) escape '\\'`;
42
+ }
43
+ if (provider === "mysql") {
44
+ return sql<boolean>`binary ${column} like binary ${pattern} escape '\\'`;
45
+ }
46
+ return sql<boolean>`${column} like ${pattern} escape '\\'`;
47
+ };
48
+
49
+ const predicate = <TModel extends DatabaseModel>(
50
+ provider: ORMSQLProvider,
51
+ condition: DatabaseWhere<TModel>,
52
+ ): RawBuilder<boolean> => {
53
+ const column = sql.ref(condition.field);
54
+ const operator = condition.operator ?? "eq";
55
+ switch (operator) {
56
+ case "eq":
57
+ case "ne": {
58
+ if (condition.value === null) {
59
+ return operator === "eq"
60
+ ? sql<boolean>`${column} is null`
61
+ : sql<boolean>`${column} is not null`;
62
+ }
63
+ const insensitive =
64
+ "mode" in condition && condition.mode === "insensitive";
65
+ if (insensitive) {
66
+ return operator === "eq"
67
+ ? sql<boolean>`lower(${column}) = lower(${condition.value})`
68
+ : sql<boolean>`lower(${column}) <> lower(${condition.value})`;
69
+ }
70
+ return operator === "eq"
71
+ ? sql<boolean>`${column} = ${condition.value}`
72
+ : sql<boolean>`${column} <> ${condition.value}`;
73
+ }
74
+ case "gt":
75
+ return sql<boolean>`${column} > ${condition.value}`;
76
+ case "gte":
77
+ return sql<boolean>`${column} >= ${condition.value}`;
78
+ case "lt":
79
+ return sql<boolean>`${column} < ${condition.value}`;
80
+ case "lte":
81
+ return sql<boolean>`${column} <= ${condition.value}`;
82
+ case "in":
83
+ case "not_in": {
84
+ if (!Array.isArray(condition.value)) {
85
+ throw new InvalidDatabasePredicateError();
86
+ }
87
+ if (operator === "in") {
88
+ return condition.value.length === 0
89
+ ? sql<boolean>`false`
90
+ : sql<boolean>`${column} in (${sql.join(condition.value)})`;
91
+ }
92
+ return condition.value.length === 0
93
+ ? sql<boolean>`true`
94
+ : sql<boolean>`${column} not in (${sql.join(condition.value)})`;
95
+ }
96
+ case "contains":
97
+ case "starts_with":
98
+ case "ends_with": {
99
+ if (typeof condition.value !== "string") {
100
+ throw new InvalidDatabasePredicateError();
101
+ }
102
+ return stringPredicate(
103
+ provider,
104
+ condition.field,
105
+ operator,
106
+ condition.value,
107
+ "mode" in condition && condition.mode === "insensitive",
108
+ );
109
+ }
110
+ }
111
+ };
112
+
113
+ export const buildKyselyWhere = <TModel extends DatabaseModel>(
114
+ provider: ORMSQLProvider,
115
+ where: readonly DatabaseWhere<TModel>[] | undefined,
116
+ ): RawBuilder<boolean> | undefined => {
117
+ const [first, ...rest] = where ?? [];
118
+ if (first === undefined) return undefined;
119
+ let expression = predicate(provider, first);
120
+ for (const condition of rest) {
121
+ const next = predicate(provider, condition);
122
+ expression =
123
+ condition.connector === "OR"
124
+ ? sql<boolean>`(${expression} or ${next})`
125
+ : sql<boolean>`(${expression} and ${next})`;
126
+ }
127
+ return expression;
128
+ };
@@ -0,0 +1,141 @@
1
+ import { createDatabaseClient } from "@hot-updater/plugin-core";
2
+ import { describe, expect, it } from "vitest";
3
+
4
+ import {
5
+ createBundlePatchRowFixture,
6
+ createBundleRowFixture,
7
+ } from "../../../test-utils/src/databaseTestFixtures";
8
+ import { setupDatabasePluginTestSuite } from "../../../test-utils/src/setupDatabasePluginTestSuite";
9
+ import { mongoAdapter } from "./mongodb";
10
+ import { createMongoBundleWhere } from "./mongodbQuery";
11
+ import { createMongoTestHarness } from "./mongodbTestClient";
12
+
13
+ const harness = createMongoTestHarness();
14
+
15
+ setupDatabasePluginTestSuite({
16
+ name: "mongoAdapter v2",
17
+ migrate: () => undefined,
18
+ createPlugin: () =>
19
+ mongoAdapter({ client: harness.client, transactions: true }),
20
+ reset: () => harness.reset(),
21
+ dispose: () => harness.close(),
22
+ });
23
+
24
+ describe("mongoAdapter capabilities", () => {
25
+ it("returns an adapter without an unsafe atomic-batch fallback", () => {
26
+ const plugin = mongoAdapter({ client: harness.client });
27
+ expect(plugin.name).toBe("mongodb");
28
+ expect(plugin.adapterName).toBe("mongodb");
29
+ expect(plugin.provider).toBe("mongodb");
30
+ expect(Reflect.has(plugin, "transaction")).toBe(false);
31
+ });
32
+
33
+ it("rejects a cross-table commit when transactions are disabled", async () => {
34
+ const plugin = mongoAdapter({ client: harness.client });
35
+ const owner = createBundleRowFixture("970");
36
+ const patch = createBundlePatchRowFixture(
37
+ "971",
38
+ owner.id,
39
+ createBundleRowFixture("972").id,
40
+ );
41
+
42
+ await expect(
43
+ plugin.commit({
44
+ changes: [
45
+ {
46
+ model: "bundles",
47
+ operation: "insert",
48
+ row: owner,
49
+ },
50
+ {
51
+ model: "bundlePatches",
52
+ operation: "insert",
53
+ row: patch,
54
+ },
55
+ ],
56
+ }),
57
+ ).rejects.toMatchObject({ name: "DatabaseAtomicCommitUnsupportedError" });
58
+ });
59
+
60
+ it("recovers a tombstoned bundle when an aggregate delete is retried", async () => {
61
+ harness.reset();
62
+ const client = createDatabaseClient(
63
+ mongoAdapter({ client: harness.client }),
64
+ );
65
+ const bundle = {
66
+ id: "bundle-retry",
67
+ platform: "ios" as const,
68
+ fileHash: "bundle-retry-hash",
69
+ gitCommitHash: null,
70
+ storageUri: "storage://bundle-retry",
71
+ archiveByteSize: 3_000_000_001,
72
+ };
73
+ await client.insertBundle(bundle);
74
+
75
+ harness.failNextBundleTombstone();
76
+ await expect(client.deleteBundleById(bundle.id)).rejects.toThrow(
77
+ "injected tombstone failure",
78
+ );
79
+ await expect(client.deleteBundleById(bundle.id)).resolves.toBeUndefined();
80
+ await expect(client.insertBundle(bundle)).resolves.toBeUndefined();
81
+ await expect(client.getBundleById(bundle.id)).resolves.toMatchObject({
82
+ id: bundle.id,
83
+ fileHash: bundle.fileHash,
84
+ storageUri: bundle.storageUri,
85
+ });
86
+ });
87
+ });
88
+
89
+ describe("MongoDB low-level predicate translation", () => {
90
+ it("composes connectors left to right", () => {
91
+ expect(
92
+ createMongoBundleWhere([
93
+ { field: "id", value: "first" },
94
+ { field: "id", value: "second", connector: "OR" },
95
+ { field: "platform", value: "ios", connector: "AND" },
96
+ ]),
97
+ ).toEqual({
98
+ $and: [
99
+ {
100
+ $or: [
101
+ { $expr: { $eq: ["$id", { $literal: "first" }] } },
102
+ { $expr: { $eq: ["$id", { $literal: "second" }] } },
103
+ ],
104
+ },
105
+ { $expr: { $eq: ["$platform", { $literal: "ios" }] } },
106
+ ],
107
+ });
108
+ });
109
+
110
+ it("escapes insensitive string patterns", () => {
111
+ expect(
112
+ createMongoBundleWhere([
113
+ {
114
+ field: "storage_uri",
115
+ operator: "contains",
116
+ value: "release.*",
117
+ mode: "insensitive",
118
+ },
119
+ ]),
120
+ ).toEqual({
121
+ $expr: {
122
+ $regexMatch: {
123
+ input: { $ifNull: ["$storage_uri", ""] },
124
+ regex: { $literal: "release\\.\\*" },
125
+ options: "i",
126
+ },
127
+ },
128
+ });
129
+ });
130
+
131
+ it("preserves empty set semantics", () => {
132
+ expect(
133
+ createMongoBundleWhere([{ field: "id", operator: "in", value: [] }]),
134
+ ).toEqual({ $expr: { $in: ["$id", { $literal: [] }] } });
135
+ expect(
136
+ createMongoBundleWhere([{ field: "id", operator: "not_in", value: [] }]),
137
+ ).toEqual({
138
+ $expr: { $not: [{ $in: ["$id", { $literal: [] }] }] },
139
+ });
140
+ });
141
+ });
@@ -1,302 +1,74 @@
1
- import { NIL_UUID } from "@hot-updater/core";
2
- import type {
3
- Bundle,
4
- DatabaseBundleQueryOptions,
5
- DatabaseBundleQueryWhere,
6
- } from "@hot-updater/plugin-core";
1
+ import { createDatabasePlugin } from "@hot-updater/plugin-core";
7
2
  import {
8
- calculatePagination,
9
- createDatabasePlugin,
10
- filterCompatibleAppVersions,
11
- resolveUpdateInfoFromBundles,
12
- } from "@hot-updater/plugin-core";
13
- import type { ClientSession, Collection, Filter, MongoClient } from "mongodb";
3
+ createDatabasePluginAdapter,
4
+ type DatabasePluginImplementation,
5
+ type TransactionDatabasePluginImplementation,
6
+ } from "@hot-updater/plugin-core/internal";
7
+ import type { ClientSession, MongoClient } from "mongodb";
14
8
 
15
- import {
16
- bundleToPatchRows,
17
- bundleToRow,
18
- type BundlePatchRow,
19
- type BundleRow,
20
- rowToBundle,
21
- } from "../db/bundleRows";
22
9
  import { createMongoMigrator } from "../db/fixedMigrator";
23
- import type { DatabasePluginFactory } from "../db/types";
10
+ import type { DatabaseAdapterWithCapabilities } from "../db/types";
11
+ import { createMongoCollections } from "./mongodbCollections";
12
+ import { createMongoReads } from "./mongodbReads";
13
+ import { createMongoWrites } from "./mongodbWrites";
24
14
 
25
15
  export interface MongoDBConfig {
26
16
  readonly client: MongoClient;
17
+ readonly transactions?: boolean;
27
18
  }
28
19
 
29
- const mongoWhere = (
30
- where: DatabaseBundleQueryWhere | undefined,
31
- ): Filter<BundleRow> => {
32
- const baseFilter: Filter<BundleRow> = {
33
- ...(where?.channel !== undefined ? { channel: where.channel } : {}),
34
- ...(where?.platform !== undefined ? { platform: where.platform } : {}),
35
- ...(where?.enabled !== undefined ? { enabled: where.enabled } : {}),
36
- ...(where?.fingerprintHash !== undefined
37
- ? where.fingerprintHash === null
38
- ? { fingerprint_hash: { $in: [null, ""] } }
39
- : { fingerprint_hash: where.fingerprintHash }
40
- : {}),
41
- ...(where?.id
42
- ? {
43
- id: {
44
- ...(where.id.eq !== undefined ? { $eq: where.id.eq } : {}),
45
- ...(where.id.gt !== undefined ? { $gt: where.id.gt } : {}),
46
- ...(where.id.gte !== undefined ? { $gte: where.id.gte } : {}),
47
- ...(where.id.lt !== undefined ? { $lt: where.id.lt } : {}),
48
- ...(where.id.lte !== undefined ? { $lte: where.id.lte } : {}),
49
- ...(where.id.in !== undefined ? { $in: where.id.in } : {}),
50
- },
51
- }
52
- : {}),
20
+ const createMongoImplementation = (
21
+ client: MongoClient,
22
+ session?: ClientSession,
23
+ ): DatabasePluginImplementation => {
24
+ const collections = createMongoCollections(client);
25
+ return {
26
+ ...createMongoWrites(collections, session),
27
+ ...createMongoReads(collections, session),
53
28
  };
54
- const targetAppVersionFilters: Filter<BundleRow>[] = [];
55
- if (where?.targetAppVersion !== undefined) {
56
- targetAppVersionFilters.push(
57
- where.targetAppVersion === null
58
- ? { target_app_version: { $in: [null, ""] } }
59
- : { target_app_version: where.targetAppVersion },
60
- );
61
- }
62
- if (where?.targetAppVersionIn) {
63
- targetAppVersionFilters.push({
64
- target_app_version: { $in: where.targetAppVersionIn },
65
- });
66
- }
67
- if (where?.targetAppVersionNotNull) {
68
- targetAppVersionFilters.push({
69
- target_app_version: { $exists: true, $nin: [null, ""] },
70
- });
71
- }
72
-
73
- const filters = [
74
- ...(Object.keys(baseFilter).length > 0 ? [baseFilter] : []),
75
- ...targetAppVersionFilters,
76
- ];
77
- if (filters.length === 0) return {};
78
- if (filters.length === 1) return filters[0] ?? {};
79
- return { $and: filters };
80
29
  };
81
30
 
82
- const createMongoPlugin = createDatabasePlugin<MongoDBConfig>({
83
- name: "mongodb",
84
- factory: ({ client }) => {
85
- const db = client.db();
86
- const bundles = db.collection<BundleRow>("bundles");
87
- const patches = db.collection<BundlePatchRow>("bundle_patches");
88
- const fetchPatchMap = async (bundleIds: readonly string[]) => {
89
- const patchMap = new Map<string, BundlePatchRow[]>();
90
- if (bundleIds.length === 0) return patchMap;
91
- const rows = await patches
92
- .find({ bundle_id: { $in: [...bundleIds] } })
93
- .sort({ order_index: 1 })
94
- .toArray();
95
- for (const row of rows) {
96
- const current = patchMap.get(row.bundle_id) ?? [];
97
- current.push(row);
98
- patchMap.set(row.bundle_id, current);
99
- }
100
- return patchMap;
101
- };
102
- const mapRowsToBundles = async (
103
- rows: readonly BundleRow[],
104
- ): Promise<Bundle[]> => {
105
- const patchMap = await fetchPatchMap(rows.map((row) => row.id));
106
- return rows.map((row) => rowToBundle(row, patchMap.get(row.id) ?? []));
107
- };
108
- const isTransactionUnsupported = (error: unknown): boolean =>
109
- error instanceof Error &&
110
- /Transaction numbers are only allowed|replica set member or mongos|Transaction API error/i.test(
111
- error.message,
112
- );
113
- const runInTransaction = async <T>(
114
- operation: (session: ClientSession | undefined) => Promise<T>,
115
- ) => {
116
- if (typeof client.startSession !== "function") {
117
- return operation(undefined);
118
- }
119
- const session = client.startSession();
120
- try {
121
- if (typeof session.withTransaction !== "function") {
122
- return await operation(session);
123
- }
124
- let result: T | undefined;
125
- await session.withTransaction(async () => {
126
- result = await operation(session);
127
- });
128
- return result as T;
129
- } catch (error) {
130
- if (isTransactionUnsupported(error)) {
131
- return operation(undefined);
132
- }
133
- throw error;
134
- } finally {
135
- await session.endSession();
136
- }
137
- };
138
- const replaceBundle = async (
139
- bundle: Bundle,
140
- session: ClientSession | undefined,
141
- ) => {
142
- const row = bundleToRow(bundle);
143
- await bundles.updateOne(
144
- { id: bundle.id },
145
- { $set: row },
146
- { session, upsert: true },
147
- );
148
- await patches.deleteMany({ bundle_id: bundle.id }, { session });
149
- const patchRows = bundleToPatchRows(bundle);
150
- if (patchRows.length > 0)
151
- await patches.insertMany(patchRows, { session });
152
- };
153
- const deleteByBundleId = async (
154
- collection: Collection<BundlePatchRow>,
155
- field: "bundle_id" | "base_bundle_id",
156
- bundleId: string,
157
- session: ClientSession | undefined,
158
- ) => {
159
- await collection.deleteMany({ [field]: bundleId }, { session });
160
- };
161
- return {
162
- async getBundleById(bundleId) {
163
- const row = await bundles.findOne({ id: bundleId });
164
- if (!row) return null;
165
- const patchMap = await fetchPatchMap([bundleId]);
166
- return rowToBundle(row, patchMap.get(bundleId) ?? []);
167
- },
168
- async getBundles(
169
- options: DatabaseBundleQueryOptions & { offset?: number },
170
- ) {
171
- const offset = options.offset ?? 0;
172
- const orderBy = options.orderBy ?? { field: "id", direction: "desc" };
173
- const where = mongoWhere(options.where);
174
- const [total, rows] = await Promise.all([
175
- bundles.countDocuments(where),
176
- bundles
177
- .find(where)
178
- .sort({ id: orderBy.direction === "asc" ? 1 : -1 })
179
- .skip(offset)
180
- .limit(options.limit)
181
- .toArray(),
182
- ]);
183
- const patchMap = await fetchPatchMap(rows.map((row) => row.id));
184
- return {
185
- data: rows.map((row) => rowToBundle(row, patchMap.get(row.id) ?? [])),
186
- pagination: calculatePagination(total, {
187
- limit: options.limit,
188
- offset,
189
- }),
190
- };
191
- },
192
- async getUpdateInfo(args, context) {
193
- if (args._updateStrategy === "appVersion") {
194
- const channel = args.channel ?? "production";
195
- const minBundleId = args.minBundleId ?? NIL_UUID;
196
- const rows = await bundles
197
- .find({
198
- enabled: true,
199
- platform: args.platform,
200
- channel,
201
- id: { $gte: minBundleId },
202
- target_app_version: { $exists: true, $nin: [null, ""] },
203
- })
204
- .project<{ target_app_version?: string | null }>({
205
- target_app_version: 1,
206
- })
207
- .toArray();
208
-
209
- const targetAppVersions = Array.from(
210
- new Set(
211
- rows
212
- .map((row) => row.target_app_version)
213
- .filter(
214
- (value): value is string =>
215
- typeof value === "string" && value.length > 0,
216
- ),
217
- ),
218
- );
219
- const compatibleAppVersions = filterCompatibleAppVersions(
220
- targetAppVersions,
221
- args.appVersion,
222
- );
223
- const updateRows =
224
- compatibleAppVersions.length > 0
225
- ? await bundles
226
- .find({
227
- enabled: true,
228
- platform: args.platform,
229
- channel,
230
- id: { $gte: minBundleId },
231
- target_app_version: { $in: compatibleAppVersions },
232
- })
233
- .sort({ id: -1 })
234
- .toArray()
235
- : [];
236
-
237
- return resolveUpdateInfoFromBundles({
238
- args: { ...args, channel, minBundleId },
239
- bundles: await mapRowsToBundles(updateRows),
240
- context,
241
- });
242
- }
243
-
244
- const channel = args.channel ?? "production";
245
- const minBundleId = args.minBundleId ?? NIL_UUID;
246
- const rows = await bundles
247
- .find({
248
- enabled: true,
249
- platform: args.platform,
250
- channel,
251
- id: { $gte: minBundleId },
252
- fingerprint_hash: args.fingerprintHash,
253
- })
254
- .sort({ id: -1 })
255
- .toArray();
256
-
257
- return resolveUpdateInfoFromBundles({
258
- args: { ...args, channel, minBundleId },
259
- bundles: await mapRowsToBundles(rows),
260
- context,
261
- });
262
- },
263
- async getChannels() {
264
- const channels = await bundles.distinct("channel");
265
- return channels
266
- .filter((channel): channel is string => typeof channel === "string")
267
- .sort((left, right) => left.localeCompare(right));
268
- },
269
- async commitBundle({ changedSets }) {
270
- await runInTransaction(async (session) => {
271
- for (const change of changedSets) {
272
- if (change.operation === "delete") {
273
- await deleteByBundleId(
274
- patches,
275
- "bundle_id",
276
- change.data.id,
277
- session,
278
- );
279
- await deleteByBundleId(
280
- patches,
281
- "base_bundle_id",
282
- change.data.id,
283
- session,
284
- );
285
- await bundles.deleteMany({ id: change.data.id }, { session });
286
- continue;
287
- }
288
- await replaceBundle(change.data, session);
289
- }
290
- });
291
- },
292
- };
293
- },
31
+ const createTransactionalMongoImplementation = (
32
+ client: MongoClient,
33
+ ): DatabasePluginImplementation => ({
34
+ ...createMongoImplementation(client),
35
+ deleteChannel: (input) =>
36
+ client.withSession((session) =>
37
+ session.withTransaction(() =>
38
+ createMongoImplementation(client, session).deleteChannel(input),
39
+ ),
40
+ ),
41
+ transaction: <TResult>(
42
+ callback: (
43
+ transaction: TransactionDatabasePluginImplementation,
44
+ ) => Promise<TResult>,
45
+ ): Promise<TResult> =>
46
+ client.withSession((session) =>
47
+ session.withTransaction(() =>
48
+ callback(createMongoImplementation(client, session)),
49
+ ),
50
+ ),
294
51
  });
295
52
 
296
- export const mongoAdapter = (config: MongoDBConfig): DatabasePluginFactory => {
297
- return Object.assign(createMongoPlugin(config), {
298
- adapterName: "mongodb",
299
- provider: "mongodb" as const,
300
- createMigrator: () => createMongoMigrator(config.client),
301
- });
53
+ export const mongoAdapter = (
54
+ config: MongoDBConfig,
55
+ ): DatabaseAdapterWithCapabilities => {
56
+ const adapter = createDatabasePluginAdapter(
57
+ "mongodb",
58
+ config.transactions === true
59
+ ? createTransactionalMongoImplementation(config.client)
60
+ : createMongoImplementation(config.client),
61
+ );
62
+ return Object.assign(
63
+ createDatabasePlugin({
64
+ name: "mongodb",
65
+ models: adapter.models,
66
+ commit: adapter.commit,
67
+ }),
68
+ {
69
+ adapterName: "mongodb",
70
+ provider: "mongodb" as const,
71
+ createMigrator: () => createMongoMigrator(config.client),
72
+ },
73
+ );
302
74
  };
@@ -0,0 +1,62 @@
1
+ import type {
2
+ BundleEventRow,
3
+ BundlePatchRow,
4
+ BundleRow,
5
+ ChannelRow,
6
+ ApiKeyRow,
7
+ ReleaseCatalogRow,
8
+ ReleaseRow,
9
+ } from "@hot-updater/plugin-core";
10
+ import type { ClientSession, Collection, MongoClient } from "mongodb";
11
+
12
+ export class MongoAdapterConstraintError extends Error {
13
+ readonly name = "MongoAdapterConstraintError";
14
+
15
+ constructor(readonly reason: string) {
16
+ super(`MongoDB adapter constraint failed: ${reason}`);
17
+ }
18
+ }
19
+
20
+ export const WITHOUT_MONGO_ID = { _id: 0 } as const;
21
+ export const DELETION_TOKEN_FIELD = "_hot_updater_deletion_token" as const;
22
+
23
+ export type MongoBundleDocument = BundleRow & {
24
+ readonly [DELETION_TOKEN_FIELD]?: string;
25
+ };
26
+
27
+ export const WITHOUT_INTERNAL_FIELDS = {
28
+ ...WITHOUT_MONGO_ID,
29
+ [DELETION_TOKEN_FIELD]: 0,
30
+ } as const;
31
+
32
+ export const activeBundleFilter = (where: object) => ({
33
+ $and: [where, { [DELETION_TOKEN_FIELD]: { $exists: false } }],
34
+ });
35
+
36
+ export type MongoCollections = {
37
+ readonly bundles: Collection<MongoBundleDocument>;
38
+ readonly bundlePatches: Collection<BundlePatchRow>;
39
+ readonly bundleEvents: Collection<BundleEventRow>;
40
+ readonly channels: Collection<ChannelRow>;
41
+ readonly apiKeys: Collection<ApiKeyRow>;
42
+ readonly releases: Collection<ReleaseRow>;
43
+ readonly releaseCatalogs: Collection<ReleaseCatalogRow>;
44
+ };
45
+
46
+ export const createMongoCollections = (
47
+ client: MongoClient,
48
+ ): MongoCollections => {
49
+ const database = client.db();
50
+ return {
51
+ bundles: database.collection<MongoBundleDocument>("bundles"),
52
+ bundlePatches: database.collection<BundlePatchRow>("bundle_patches"),
53
+ bundleEvents: database.collection<BundleEventRow>("bundle_events"),
54
+ channels: database.collection<ChannelRow>("channels"),
55
+ apiKeys: database.collection<ApiKeyRow>("api_keys"),
56
+ releases: database.collection<ReleaseRow>("releases"),
57
+ releaseCatalogs: database.collection<ReleaseCatalogRow>("release_catalogs"),
58
+ };
59
+ };
60
+
61
+ export const mongoSessionOptions = (session?: ClientSession) =>
62
+ session === undefined ? {} : { session };