@hot-updater/server 0.36.6 → 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
@@ -0,0 +1,118 @@
1
+ import {
2
+ isDatabaseMetadataObject,
3
+ type BundlePatchRow,
4
+ type BundleRow,
5
+ type ChannelRow,
6
+ } from "@hot-updater/plugin-core";
7
+
8
+ export class MongoAdapterDataError extends Error {
9
+ readonly name = "MongoAdapterDataError";
10
+
11
+ constructor(readonly source: string) {
12
+ super(`Invalid MongoDB plugin data at "${source}".`);
13
+ }
14
+ }
15
+
16
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
17
+ typeof value === "object" && value !== null && !Array.isArray(value);
18
+
19
+ const record = (value: unknown, source: string): Record<string, unknown> => {
20
+ if (!isRecord(value)) {
21
+ throw new MongoAdapterDataError(source);
22
+ }
23
+ return value;
24
+ };
25
+
26
+ const string = (value: unknown, source: string): string => {
27
+ if (typeof value !== "string") throw new MongoAdapterDataError(source);
28
+ return value;
29
+ };
30
+
31
+ const nullableString = (value: unknown, source: string): string | null => {
32
+ if (value === null || value === undefined) return null;
33
+ return string(value, source);
34
+ };
35
+
36
+ const integer = (
37
+ value: unknown,
38
+ source: string,
39
+ maximum = Number.POSITIVE_INFINITY,
40
+ ): number => {
41
+ if (
42
+ typeof value !== "number" ||
43
+ !Number.isInteger(value) ||
44
+ value < 0 ||
45
+ value > maximum
46
+ ) {
47
+ throw new MongoAdapterDataError(source);
48
+ }
49
+ return value;
50
+ };
51
+
52
+ const platform = (value: unknown, source: string): "android" | "ios" => {
53
+ if (value !== "android" && value !== "ios") {
54
+ throw new MongoAdapterDataError(source);
55
+ }
56
+ return value;
57
+ };
58
+
59
+ const metadata = (value: unknown, source: string) => {
60
+ if (!isDatabaseMetadataObject(value)) {
61
+ throw new MongoAdapterDataError(source);
62
+ }
63
+ return value;
64
+ };
65
+
66
+ export const parseMongoBundleRow = (
67
+ value: unknown,
68
+ source = "bundles",
69
+ ): BundleRow => {
70
+ const input = record(value, source);
71
+ return {
72
+ id: string(input["id"], source),
73
+ platform: platform(input["platform"], source),
74
+ file_hash: string(input["file_hash"], source),
75
+ git_commit_hash: nullableString(input["git_commit_hash"], source),
76
+ storage_uri: string(input["storage_uri"], source),
77
+ archive_byte_size: integer(
78
+ input["archive_byte_size"],
79
+ source,
80
+ Number.MAX_SAFE_INTEGER,
81
+ ),
82
+ metadata: metadata(input["metadata"], source),
83
+ manifest_storage_uri: nullableString(input["manifest_storage_uri"], source),
84
+ manifest_file_hash: nullableString(input["manifest_file_hash"], source),
85
+ asset_base_storage_uri: nullableString(
86
+ input["asset_base_storage_uri"],
87
+ source,
88
+ ),
89
+ };
90
+ };
91
+
92
+ export const parseMongoChannelRow = (
93
+ value: unknown,
94
+ source = "channels",
95
+ ): ChannelRow => {
96
+ const input = record(value, source);
97
+ return {
98
+ id: string(input["id"], source),
99
+ name: string(input["name"], source),
100
+ };
101
+ };
102
+
103
+ export const parseMongoPatchRow = (
104
+ value: unknown,
105
+ source = "bundle_patches",
106
+ ): BundlePatchRow => {
107
+ const input = record(value, source);
108
+ return {
109
+ id: string(input["id"], source),
110
+ bundle_id: string(input["bundle_id"], source),
111
+ base_bundle_id: string(input["base_bundle_id"], source),
112
+ base_file_hash: string(input["base_file_hash"], source),
113
+ patch_file_hash: string(input["patch_file_hash"], source),
114
+ patch_storage_uri: string(input["patch_storage_uri"], source),
115
+ byte_size: integer(input["byte_size"], source, Number.MAX_SAFE_INTEGER),
116
+ order_index: integer(input["order_index"], source),
117
+ };
118
+ };
@@ -0,0 +1,342 @@
1
+ import type { BundlePatchRow, BundleRow } from "@hot-updater/plugin-core";
2
+ import type { ClientSession } from "mongodb";
3
+ import { MongoClient } from "mongodb";
4
+
5
+ import {
6
+ matchesMongoTestFilter,
7
+ type MongoTestRow,
8
+ sortMongoTestRows,
9
+ } from "./mongodbTestFilter";
10
+
11
+ type Tables = {
12
+ bundle_patches: MongoTestRow[];
13
+ bundles: MongoTestRow[];
14
+ bundle_events: MongoTestRow[];
15
+ channels: MongoTestRow[];
16
+ api_keys: MongoTestRow[];
17
+ releases: MongoTestRow[];
18
+ release_catalogs: MongoTestRow[];
19
+ };
20
+
21
+ type FindOptions = { readonly projection?: unknown };
22
+ type UpdateInput =
23
+ | { readonly $set: Readonly<Record<string, unknown>> }
24
+ | { readonly $setOnInsert: MongoTestRow };
25
+ type MongoTestHooks = {
26
+ beforeBundlePatchInsert?: () => Promise<void>;
27
+ failNextBundleTombstone: boolean;
28
+ operationCount: number;
29
+ };
30
+
31
+ class MongoTestConstraintError extends Error {
32
+ readonly name = "MongoTestConstraintError";
33
+ }
34
+
35
+ class MongoTestCursor {
36
+ private offset = 0;
37
+ private maximum = Number.POSITIVE_INFINITY;
38
+ private sortSpecification: unknown;
39
+
40
+ constructor(
41
+ private readonly rows: MongoTestRow[],
42
+ private readonly projection?: unknown,
43
+ ) {}
44
+
45
+ limit(value: number): this {
46
+ this.maximum = value;
47
+ return this;
48
+ }
49
+
50
+ project(): this {
51
+ return this;
52
+ }
53
+
54
+ skip(value: number): this {
55
+ this.offset = value;
56
+ return this;
57
+ }
58
+
59
+ sort(value: unknown): this {
60
+ this.sortSpecification = value;
61
+ return this;
62
+ }
63
+
64
+ async toArray(): Promise<MongoTestRow[]> {
65
+ const rows = sortMongoTestRows(this.rows, this.sortSpecification);
66
+ return structuredClone(
67
+ rows
68
+ .slice(this.offset, this.offset + this.maximum)
69
+ .map((row) => projectMongoTestRow(row, this.projection)),
70
+ );
71
+ }
72
+ }
73
+
74
+ const projectMongoTestRow = (
75
+ row: MongoTestRow,
76
+ projection: unknown,
77
+ ): MongoTestRow => {
78
+ if (typeof projection !== "object" || projection === null) return row;
79
+ const projected = structuredClone(row);
80
+ for (const field of Object.keys(projected)) {
81
+ if (Reflect.get(projection, field) === 0) {
82
+ Reflect.deleteProperty(projected, field);
83
+ }
84
+ }
85
+ return projected;
86
+ };
87
+
88
+ const createCollection = (
89
+ tables: Tables,
90
+ model: keyof Tables,
91
+ hooks: MongoTestHooks,
92
+ ) => ({
93
+ countDocuments: async (filter?: unknown): Promise<number> => {
94
+ hooks.operationCount += 1;
95
+ return tables[model].filter((row) => matchesMongoTestFilter(row, filter))
96
+ .length;
97
+ },
98
+ deleteMany: async (filter?: unknown): Promise<void> => {
99
+ hooks.operationCount += 1;
100
+ tables[model] = tables[model].filter(
101
+ (row) => !matchesMongoTestFilter(row, filter),
102
+ );
103
+ },
104
+ distinct: async (field: string, filter?: unknown): Promise<unknown[]> => {
105
+ hooks.operationCount += 1;
106
+ return Array.from(
107
+ new Set(
108
+ tables[model]
109
+ .filter((row) => matchesMongoTestFilter(row, filter))
110
+ .map((row) => Reflect.get(row, field)),
111
+ ),
112
+ );
113
+ },
114
+ find: (filter?: unknown, options?: FindOptions): MongoTestCursor => {
115
+ hooks.operationCount += 1;
116
+ return new MongoTestCursor(
117
+ tables[model].filter((row) => matchesMongoTestFilter(row, filter)),
118
+ options?.projection,
119
+ );
120
+ },
121
+ findOne: async (
122
+ filter?: unknown,
123
+ options?: FindOptions,
124
+ ): Promise<MongoTestRow | null> => {
125
+ hooks.operationCount += 1;
126
+ const row = tables[model].find((candidate) =>
127
+ matchesMongoTestFilter(candidate, filter),
128
+ );
129
+ return row === undefined
130
+ ? null
131
+ : structuredClone(projectMongoTestRow(row, options?.projection));
132
+ },
133
+ findOneAndUpdate: async (
134
+ filter: unknown,
135
+ update: UpdateInput,
136
+ ): Promise<MongoTestRow | null> => {
137
+ hooks.operationCount += 1;
138
+ const index = tables[model].findIndex((row) =>
139
+ matchesMongoTestFilter(row, filter),
140
+ );
141
+ const current = tables[model][index];
142
+ if (current === undefined) return null;
143
+ if (!("$set" in update)) {
144
+ throw new MongoTestConstraintError("findOneAndUpdate requires $set");
145
+ }
146
+ const updated = { ...current, ...update.$set } as MongoTestRow;
147
+ tables[model][index] = updated;
148
+ return structuredClone(updated);
149
+ },
150
+ insertOne: async (row: MongoTestRow): Promise<void> => {
151
+ hooks.operationCount += 1;
152
+ if (model === "bundle_patches") await hooks.beforeBundlePatchInsert?.();
153
+ const key = "id" in row ? row.id : row.scope_key;
154
+ if (
155
+ tables[model].some((candidate) =>
156
+ "id" in candidate ? candidate.id === key : candidate.scope_key === key,
157
+ )
158
+ ) {
159
+ throw new MongoTestConstraintError("duplicate id");
160
+ }
161
+ if (
162
+ model === "channels" &&
163
+ "name" in row &&
164
+ tables.channels.some(
165
+ (candidate) => "name" in candidate && candidate.name === row.name,
166
+ )
167
+ ) {
168
+ throw new MongoTestConstraintError("duplicate channel name");
169
+ }
170
+ if (
171
+ model === "api_keys" &&
172
+ "hash" in row &&
173
+ tables.api_keys.some(
174
+ (candidate) => "hash" in candidate && candidate.hash === row.hash,
175
+ )
176
+ ) {
177
+ throw new MongoTestConstraintError("duplicate hash");
178
+ }
179
+ tables[model].push(structuredClone(row));
180
+ },
181
+ updateMany: async (filter: unknown, update: UpdateInput): Promise<void> => {
182
+ hooks.operationCount += 1;
183
+ const values = "$set" in update ? update.$set : {};
184
+ tables[model] = tables[model].map((row) =>
185
+ matchesMongoTestFilter(row, filter)
186
+ ? ({ ...row, ...values } as MongoTestRow)
187
+ : row,
188
+ );
189
+ if (model === "bundles" && hooks.failNextBundleTombstone) {
190
+ hooks.failNextBundleTombstone = false;
191
+ throw new MongoTestConstraintError("injected tombstone failure");
192
+ }
193
+ },
194
+ updateOne: async (
195
+ filter: unknown,
196
+ update: UpdateInput,
197
+ options?: { readonly upsert?: boolean },
198
+ ): Promise<{ matchedCount: number; upsertedCount: number }> => {
199
+ hooks.operationCount += 1;
200
+ const index = tables[model].findIndex((row) =>
201
+ matchesMongoTestFilter(row, filter),
202
+ );
203
+ if (index >= 0) {
204
+ if ("$set" in update) {
205
+ tables[model][index] = {
206
+ ...tables[model][index],
207
+ ...update.$set,
208
+ } as MongoTestRow;
209
+ }
210
+ return { matchedCount: 1, upsertedCount: 0 };
211
+ }
212
+ if (options?.upsert === true && "$setOnInsert" in update) {
213
+ const key =
214
+ "id" in update.$setOnInsert
215
+ ? update.$setOnInsert.id
216
+ : update.$setOnInsert.scope_key;
217
+ if (
218
+ tables[model].some((row) =>
219
+ "id" in row ? row.id === key : row.scope_key === key,
220
+ )
221
+ ) {
222
+ throw new MongoTestConstraintError("duplicate id");
223
+ }
224
+ tables[model].push(structuredClone(update.$setOnInsert));
225
+ return { matchedCount: 0, upsertedCount: 1 };
226
+ }
227
+ return { matchedCount: 0, upsertedCount: 0 };
228
+ },
229
+ });
230
+
231
+ const createDatabase = (tables: Tables, hooks: MongoTestHooks) => ({
232
+ collection: (name: string) => {
233
+ switch (name) {
234
+ case "bundles":
235
+ return createCollection(tables, "bundles", hooks);
236
+ case "bundle_patches":
237
+ return createCollection(tables, "bundle_patches", hooks);
238
+ case "bundle_events":
239
+ return createCollection(tables, "bundle_events", hooks);
240
+ case "channels":
241
+ return createCollection(tables, "channels", hooks);
242
+ case "api_keys":
243
+ return createCollection(tables, "api_keys", hooks);
244
+ case "releases":
245
+ return createCollection(tables, "releases", hooks);
246
+ case "release_catalogs":
247
+ return createCollection(tables, "release_catalogs", hooks);
248
+ default:
249
+ throw new MongoTestConstraintError(`unknown collection: ${name}`);
250
+ }
251
+ },
252
+ });
253
+
254
+ export const createMongoTestHarness = () => {
255
+ const tables: Tables = {
256
+ bundle_patches: [],
257
+ bundles: [],
258
+ bundle_events: [],
259
+ channels: [],
260
+ api_keys: [],
261
+ releases: [],
262
+ release_catalogs: [],
263
+ };
264
+ const hooks: MongoTestHooks = {
265
+ failNextBundleTombstone: false,
266
+ operationCount: 0,
267
+ };
268
+ let activeTables = tables;
269
+ let transactionQueue = Promise.resolve();
270
+ const client = new MongoClient("mongodb://127.0.0.1:27017/hot_updater_test");
271
+ Object.defineProperty(client, "db", {
272
+ value: () => createDatabase(activeTables, hooks),
273
+ });
274
+ Object.defineProperty(client, "withSession", {
275
+ value: async (
276
+ callback: (session: ClientSession) => Promise<unknown>,
277
+ ): Promise<unknown> => {
278
+ const session = {
279
+ withTransaction: async (transaction: () => Promise<unknown>) => {
280
+ const previous = transactionQueue;
281
+ let release!: () => void;
282
+ transactionQueue = new Promise<void>((resolve) => {
283
+ release = resolve;
284
+ });
285
+ await previous;
286
+ try {
287
+ const staged = structuredClone(tables);
288
+ activeTables = staged;
289
+ const result = await transaction();
290
+ tables.bundle_patches = staged.bundle_patches;
291
+ tables.bundles = staged.bundles;
292
+ tables.bundle_events = staged.bundle_events;
293
+ tables.channels = staged.channels;
294
+ tables.api_keys = staged.api_keys;
295
+ tables.releases = staged.releases;
296
+ tables.release_catalogs = staged.release_catalogs;
297
+ return result;
298
+ } finally {
299
+ activeTables = tables;
300
+ release();
301
+ }
302
+ },
303
+ } as unknown as ClientSession;
304
+ return callback(session);
305
+ },
306
+ });
307
+ return {
308
+ client,
309
+ close: () => client.close(),
310
+ reset: (): void => {
311
+ hooks.failNextBundleTombstone = false;
312
+ hooks.operationCount = 0;
313
+ transactionQueue = Promise.resolve();
314
+ tables.bundle_patches = [];
315
+ tables.bundles = [];
316
+ tables.bundle_events = [];
317
+ tables.channels = [];
318
+ tables.api_keys = [];
319
+ tables.releases = [];
320
+ tables.release_catalogs = [];
321
+ },
322
+ getOperationCount: (): number => hooks.operationCount,
323
+ setBeforeBundlePatchInsert: (
324
+ hook: MongoTestHooks["beforeBundlePatchInsert"],
325
+ ): void => {
326
+ hooks.beforeBundlePatchInsert = hook;
327
+ },
328
+ failNextBundleTombstone: (): void => {
329
+ hooks.failNextBundleTombstone = true;
330
+ },
331
+ setBundleField: (id: string, field: string, value: unknown): void => {
332
+ const row = tables.bundles.find(
333
+ (candidate) => "id" in candidate && candidate.id === id,
334
+ );
335
+ if (row === undefined) throw new MongoTestConstraintError("missing row");
336
+ Reflect.set(row, field, value);
337
+ },
338
+ };
339
+ };
340
+
341
+ export type MongoTestBundleRow = BundleRow;
342
+ export type MongoTestBundlePatchRow = BundlePatchRow;
@@ -0,0 +1,166 @@
1
+ import type {
2
+ BundleEventRow,
3
+ BundlePatchRow,
4
+ BundleRow,
5
+ ChannelRow,
6
+ ApiKeyRow,
7
+ ReleaseCatalogRow,
8
+ ReleaseRow,
9
+ } from "@hot-updater/plugin-core";
10
+
11
+ export type MongoTestRow =
12
+ | BundleEventRow
13
+ | BundlePatchRow
14
+ | BundleRow
15
+ | ChannelRow
16
+ | ApiKeyRow
17
+ | ReleaseRow
18
+ | ReleaseCatalogRow;
19
+
20
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
21
+ typeof value === "object" && value !== null;
22
+
23
+ export const readMongoTestField = (row: MongoTestRow, field: string): unknown =>
24
+ Object.entries(row).find(([key]) => key === field)?.[1];
25
+
26
+ const compare = (left: unknown, right: unknown): number => {
27
+ if (typeof left === "number" && typeof right === "number") {
28
+ return left - right;
29
+ }
30
+ if (typeof left === "string" && typeof right === "string") {
31
+ return left.localeCompare(right);
32
+ }
33
+ return 0;
34
+ };
35
+
36
+ const resolveExpression = (row: MongoTestRow, expression: unknown): unknown => {
37
+ if (typeof expression === "string" && expression.startsWith("$")) {
38
+ return readMongoTestField(row, expression.slice(1));
39
+ }
40
+ if (Array.isArray(expression)) {
41
+ return expression.map((item) => resolveExpression(row, item));
42
+ }
43
+ if (!isRecord(expression)) return expression;
44
+ if ("$literal" in expression) return expression["$literal"];
45
+ if ("$ifNull" in expression && Array.isArray(expression["$ifNull"])) {
46
+ const [value, fallback] = expression["$ifNull"];
47
+ const resolved = resolveExpression(row, value);
48
+ return resolved === null || resolved === undefined
49
+ ? resolveExpression(row, fallback)
50
+ : resolved;
51
+ }
52
+ if ("$toLower" in expression) {
53
+ const value = resolveExpression(row, expression["$toLower"]);
54
+ return typeof value === "string" ? value.toLocaleLowerCase() : value;
55
+ }
56
+ for (const operator of ["$eq", "$ne", "$gt", "$gte", "$lt", "$lte"]) {
57
+ const operands = expression[operator];
58
+ if (!Array.isArray(operands)) continue;
59
+ const [left, right] = operands.map((item) => resolveExpression(row, item));
60
+ switch (operator) {
61
+ case "$eq":
62
+ return Object.is(left, right);
63
+ case "$ne":
64
+ return !Object.is(left, right);
65
+ case "$gt":
66
+ return compare(left, right) > 0;
67
+ case "$gte":
68
+ return compare(left, right) >= 0;
69
+ case "$lt":
70
+ return compare(left, right) < 0;
71
+ case "$lte":
72
+ return compare(left, right) <= 0;
73
+ }
74
+ }
75
+ if ("$in" in expression && Array.isArray(expression["$in"])) {
76
+ const [value, candidates] = expression["$in"].map((item) =>
77
+ resolveExpression(row, item),
78
+ );
79
+ return (
80
+ Array.isArray(candidates) &&
81
+ candidates.some((candidate) => Object.is(candidate, value))
82
+ );
83
+ }
84
+ if ("$not" in expression && Array.isArray(expression["$not"])) {
85
+ return !resolveExpression(row, expression["$not"][0]);
86
+ }
87
+ const regexMatch = expression["$regexMatch"];
88
+ if (isRecord(regexMatch)) {
89
+ const input = resolveExpression(row, regexMatch["input"]);
90
+ const regex = resolveExpression(row, regexMatch["regex"]);
91
+ const options = regexMatch["options"];
92
+ return (
93
+ typeof input === "string" &&
94
+ typeof regex === "string" &&
95
+ new RegExp(regex, options === "i" ? "i" : undefined).test(input)
96
+ );
97
+ }
98
+ return expression;
99
+ };
100
+
101
+ const matchesField = (current: unknown, condition: unknown): boolean => {
102
+ if (!isRecord(condition)) return Object.is(current, condition);
103
+ if (typeof condition["$exists"] === "boolean") {
104
+ return condition["$exists"] ? current !== undefined : current === undefined;
105
+ }
106
+ if (Array.isArray(condition["$in"])) {
107
+ return condition["$in"].some((item) => Object.is(item, current));
108
+ }
109
+ if ("$ne" in condition && Object.is(current, condition["$ne"])) {
110
+ return false;
111
+ }
112
+ if ("$gte" in condition && compare(current, condition["$gte"]) < 0) {
113
+ return false;
114
+ }
115
+ if ("$gt" in condition && compare(current, condition["$gt"]) <= 0) {
116
+ return false;
117
+ }
118
+ if ("$lte" in condition && compare(current, condition["$lte"]) > 0) {
119
+ return false;
120
+ }
121
+ if ("$lt" in condition && compare(current, condition["$lt"]) >= 0) {
122
+ return false;
123
+ }
124
+ return true;
125
+ };
126
+
127
+ export const matchesMongoTestFilter = (
128
+ row: MongoTestRow,
129
+ filter: unknown,
130
+ ): boolean => {
131
+ if (!isRecord(filter)) return true;
132
+ if ("$expr" in filter)
133
+ return Boolean(resolveExpression(row, filter["$expr"]));
134
+ const conjunction = filter["$and"];
135
+ if (Array.isArray(conjunction)) {
136
+ return conjunction.every((item) => matchesMongoTestFilter(row, item));
137
+ }
138
+ const disjunction = filter["$or"];
139
+ if (Array.isArray(disjunction)) {
140
+ return disjunction.some((item) => matchesMongoTestFilter(row, item));
141
+ }
142
+ return Object.entries(filter).every(([field, condition]) =>
143
+ matchesField(readMongoTestField(row, field), condition),
144
+ );
145
+ };
146
+
147
+ export const sortMongoTestRows = (
148
+ rows: MongoTestRow[],
149
+ sort: unknown,
150
+ ): MongoTestRow[] => {
151
+ if (!isRecord(sort)) return rows;
152
+ const entries = Object.entries(sort);
153
+ if (entries.length === 0) return rows;
154
+ return rows.toSorted((left, right) => {
155
+ for (const [field, direction] of entries) {
156
+ const result = compare(
157
+ readMongoTestField(left, field),
158
+ readMongoTestField(right, field),
159
+ );
160
+ if (result !== 0) {
161
+ return direction === -1 ? -result : result;
162
+ }
163
+ }
164
+ return 0;
165
+ });
166
+ };