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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (291) hide show
  1. package/dist/adapters/databaseConstraintErrors.cjs +23 -0
  2. package/dist/adapters/databaseConstraintErrors.mjs +21 -0
  3. package/dist/adapters/databasePluginUtils.cjs +117 -0
  4. package/dist/adapters/databasePluginUtils.mjs +106 -0
  5. package/dist/adapters/drizzle.cjs +53 -176
  6. package/dist/adapters/drizzle.d.cts +6 -4
  7. package/dist/adapters/drizzle.d.mts +6 -4
  8. package/dist/adapters/drizzle.mjs +54 -177
  9. package/dist/adapters/drizzleCrud.cjs +250 -0
  10. package/dist/adapters/drizzleCrud.mjs +249 -0
  11. package/dist/adapters/drizzleLazyDB.cjs +84 -36
  12. package/dist/adapters/drizzleLazyDB.mjs +84 -36
  13. package/dist/adapters/drizzleQuery.cjs +68 -0
  14. package/dist/adapters/drizzleQuery.mjs +67 -0
  15. package/dist/adapters/kysely.cjs +18 -138
  16. package/dist/adapters/kysely.d.cts +3 -12
  17. package/dist/adapters/kysely.d.mts +3 -12
  18. package/dist/adapters/kysely.mjs +19 -139
  19. package/dist/adapters/kyselyCrud.cjs +254 -0
  20. package/dist/adapters/kyselyCrud.mjs +253 -0
  21. package/dist/adapters/kyselyQuery.cjs +56 -0
  22. package/dist/adapters/kyselyQuery.mjs +55 -0
  23. package/dist/adapters/mongodb.cjs +21 -174
  24. package/dist/adapters/mongodb.d.cts +3 -2
  25. package/dist/adapters/mongodb.d.mts +3 -2
  26. package/dist/adapters/mongodb.mjs +21 -174
  27. package/dist/adapters/mongodbCollections.cjs +36 -0
  28. package/dist/adapters/mongodbCollections.mjs +30 -0
  29. package/dist/adapters/mongodbQuery.cjs +77 -0
  30. package/dist/adapters/mongodbQuery.mjs +70 -0
  31. package/dist/adapters/mongodbReads.cjs +148 -0
  32. package/dist/adapters/mongodbReads.mjs +147 -0
  33. package/dist/adapters/mongodbWrites.cjs +134 -0
  34. package/dist/adapters/mongodbWrites.mjs +133 -0
  35. package/dist/adapters/prisma.cjs +205 -180
  36. package/dist/adapters/prisma.d.cts +3 -3
  37. package/dist/adapters/prisma.d.mts +3 -3
  38. package/dist/adapters/prisma.mjs +206 -181
  39. package/dist/adapters/prismaQuery.cjs +58 -0
  40. package/dist/adapters/prismaQuery.mjs +56 -0
  41. package/dist/adapters/prismaRows.cjs +198 -0
  42. package/dist/adapters/prismaRows.mjs +188 -0
  43. package/dist/analytics/bounded/activeOverview.cjs +87 -0
  44. package/dist/analytics/bounded/activeOverview.mjs +85 -0
  45. package/dist/analytics/bounded/installationSearch.cjs +42 -0
  46. package/dist/analytics/bounded/installationSearch.mjs +42 -0
  47. package/dist/analytics/bounded/persistence.cjs +39 -0
  48. package/dist/analytics/bounded/persistence.mjs +38 -0
  49. package/dist/analytics/bounded/provider.cjs +170 -0
  50. package/dist/analytics/bounded/provider.d.cts +7 -0
  51. package/dist/analytics/bounded/provider.d.mts +7 -0
  52. package/dist/analytics/bounded/provider.mjs +170 -0
  53. package/dist/analytics/bounded/scan.cjs +144 -0
  54. package/dist/analytics/bounded/scan.mjs +138 -0
  55. package/dist/analytics/domain.d.cts +121 -0
  56. package/dist/analytics/domain.d.mts +121 -0
  57. package/dist/analytics/errors.cjs +22 -0
  58. package/dist/analytics/errors.mjs +20 -0
  59. package/dist/analytics/eventInput.cjs +111 -0
  60. package/dist/analytics/eventInput.mjs +111 -0
  61. package/dist/analytics/persistence.d.cts +6 -0
  62. package/dist/analytics/persistence.d.mts +6 -0
  63. package/dist/analytics/queryInput.cjs +58 -0
  64. package/dist/analytics/queryInput.mjs +55 -0
  65. package/dist/analytics/routes.cjs +63 -0
  66. package/dist/analytics/routes.mjs +61 -0
  67. package/dist/analytics/types.d.cts +17 -0
  68. package/dist/analytics/types.d.mts +17 -0
  69. package/dist/apiKeys.cjs +139 -0
  70. package/dist/apiKeys.d.cts +50 -0
  71. package/dist/apiKeys.d.mts +50 -0
  72. package/dist/apiKeys.mjs +133 -0
  73. package/dist/createHotUpdaterCore.cjs +48 -26
  74. package/dist/createHotUpdaterCore.d.cts +49 -17
  75. package/dist/createHotUpdaterCore.d.mts +49 -17
  76. package/dist/createHotUpdaterCore.mjs +51 -29
  77. package/dist/db/createBundleDiff.cjs +45 -68
  78. package/dist/db/createBundleDiff.d.cts +3 -3
  79. package/dist/db/createBundleDiff.d.mts +3 -3
  80. package/dist/db/createBundleDiff.mjs +47 -68
  81. package/dist/db/databasePluginCore.cjs +138 -0
  82. package/dist/db/databasePluginCore.mjs +137 -0
  83. package/dist/db/fixedMigrator.cjs +36 -93
  84. package/dist/db/fixedMigrator.mjs +36 -93
  85. package/dist/db/fixedMigratorMongo.cjs +319 -0
  86. package/dist/db/fixedMigratorMongo.mjs +319 -0
  87. package/dist/db/fixedMigratorShared.cjs +23 -0
  88. package/dist/db/fixedMigratorShared.mjs +18 -0
  89. package/dist/db/mongoMigrationExecution.cjs +11 -0
  90. package/dist/db/mongoMigrationExecution.mjs +11 -0
  91. package/dist/db/releaseCatalog.cjs +66 -0
  92. package/dist/db/releaseCatalog.d.cts +17 -0
  93. package/dist/db/releaseCatalog.d.mts +17 -0
  94. package/dist/db/releaseCatalog.mjs +64 -0
  95. package/dist/db/schema/mongodb.cjs +14 -6
  96. package/dist/db/schema/mongodb.mjs +14 -6
  97. package/dist/db/schema/registry.cjs +0 -2
  98. package/dist/db/schema/registry.mjs +1 -2
  99. package/dist/db/schema/sql.cjs +31 -14
  100. package/dist/db/schema/sql.mjs +32 -13
  101. package/dist/db/schema/sqlOperations.cjs +4 -4
  102. package/dist/db/schema/sqlOperations.mjs +5 -5
  103. package/dist/db/schemaGenerators.cjs +52 -18
  104. package/dist/db/schemaGenerators.mjs +52 -18
  105. package/dist/db/schemaReadiness.cjs +4 -3
  106. package/dist/db/schemaReadiness.mjs +4 -3
  107. package/dist/db/sqlMigrationExecution.cjs +67 -0
  108. package/dist/db/sqlMigrationExecution.mjs +66 -0
  109. package/dist/db/types.cjs +2 -6
  110. package/dist/db/types.d.cts +60 -16
  111. package/dist/db/types.d.mts +60 -16
  112. package/dist/db/types.mjs +3 -6
  113. package/dist/db/updateArtifacts.cjs +114 -60
  114. package/dist/db/updateArtifacts.mjs +117 -62
  115. package/dist/handler.cjs +108 -282
  116. package/dist/handler.d.cts +3 -46
  117. package/dist/handler.d.mts +3 -46
  118. package/dist/handler.mjs +107 -281
  119. package/dist/handlerBundleRoutes.cjs +155 -0
  120. package/dist/handlerBundleRoutes.mjs +155 -0
  121. package/dist/handlerErrors.cjs +9 -0
  122. package/dist/handlerErrors.mjs +9 -0
  123. package/dist/handlerParameters.cjs +39 -0
  124. package/dist/handlerParameters.mjs +34 -0
  125. package/dist/handlerReleaseCatalogRoutes.cjs +111 -0
  126. package/dist/handlerReleaseCatalogRoutes.mjs +110 -0
  127. package/dist/handlerReleaseManagementRoutes.cjs +125 -0
  128. package/dist/handlerReleaseManagementRoutes.mjs +124 -0
  129. package/dist/handlerTypes.d.cts +63 -0
  130. package/dist/handlerTypes.d.mts +63 -0
  131. package/dist/handlerVersionRoutes.cjs +10 -0
  132. package/dist/handlerVersionRoutes.d.cts +4 -0
  133. package/dist/handlerVersionRoutes.d.mts +4 -0
  134. package/dist/handlerVersionRoutes.mjs +9 -0
  135. package/dist/index.cjs +10 -1
  136. package/dist/index.d.cts +9 -3
  137. package/dist/index.d.mts +9 -3
  138. package/dist/index.mjs +5 -2
  139. package/dist/node.cjs +19 -12
  140. package/dist/node.d.cts +15 -10
  141. package/dist/node.d.mts +15 -10
  142. package/dist/node.mjs +19 -13
  143. package/dist/package.cjs +1 -1
  144. package/dist/package.mjs +1 -1
  145. package/dist/schema/dsl.cjs +19 -2
  146. package/dist/schema/dsl.mjs +17 -3
  147. package/dist/schema/index.cjs +1 -8
  148. package/dist/schema/index.mjs +2 -8
  149. package/dist/schema/types.cjs +3 -1
  150. package/dist/schema/types.mjs +3 -2
  151. package/dist/schema/v1_0_0.cjs +364 -0
  152. package/dist/schema/v1_0_0.mjs +357 -0
  153. package/dist/storageAccess.cjs +69 -25
  154. package/dist/storageAccess.mjs +68 -25
  155. package/dist/types/index.d.cts +4 -3
  156. package/dist/types/index.d.mts +5 -4
  157. package/package.json +8 -7
  158. package/src/adapters/databaseConstraintErrors.ts +25 -0
  159. package/src/adapters/databasePluginTestDatabase.ts +106 -0
  160. package/src/adapters/databasePluginUtils.spec.ts +29 -0
  161. package/src/adapters/databasePluginUtils.ts +214 -0
  162. package/src/adapters/drizzle.spec.ts +208 -47
  163. package/src/adapters/drizzle.ts +106 -293
  164. package/src/adapters/drizzleCrud.ts +419 -0
  165. package/src/adapters/drizzleLazyDB.ts +206 -100
  166. package/src/adapters/drizzleQuery.ts +164 -0
  167. package/src/adapters/kysely.spec.ts +126 -88
  168. package/src/adapters/kysely.ts +76 -334
  169. package/src/adapters/kyselyCrud.ts +588 -0
  170. package/src/adapters/kyselyQuery.ts +128 -0
  171. package/src/adapters/mongodb.spec.ts +141 -0
  172. package/src/adapters/mongodb.ts +60 -288
  173. package/src/adapters/mongodbCollections.ts +62 -0
  174. package/src/adapters/mongodbQuery.ts +226 -0
  175. package/src/adapters/mongodbReads.ts +311 -0
  176. package/src/adapters/mongodbRows.spec.ts +64 -0
  177. package/src/adapters/mongodbRows.ts +118 -0
  178. package/src/adapters/mongodbTestClient.ts +342 -0
  179. package/src/adapters/mongodbTestFilter.ts +166 -0
  180. package/src/adapters/mongodbWrites.ts +260 -0
  181. package/src/adapters/prisma.spec.ts +196 -0
  182. package/src/adapters/prisma.ts +417 -292
  183. package/src/adapters/prismaQuery.ts +129 -0
  184. package/src/adapters/prismaRows.spec.ts +98 -0
  185. package/src/adapters/prismaRows.ts +301 -0
  186. package/src/adapters/prismaTestClient.ts +396 -0
  187. package/src/analytics/analytics.integration.spec.ts +144 -0
  188. package/src/analytics/bounded/activeOverview.spec.ts +189 -0
  189. package/src/analytics/bounded/activeOverview.ts +148 -0
  190. package/src/analytics/bounded/installationSearch.spec.ts +130 -0
  191. package/src/analytics/bounded/installationSearch.ts +69 -0
  192. package/src/analytics/bounded/persistence.ts +43 -0
  193. package/src/analytics/bounded/provider.spec.ts +219 -0
  194. package/src/analytics/bounded/provider.ts +229 -0
  195. package/src/analytics/bounded/scan.ts +235 -0
  196. package/src/analytics/domain.ts +139 -0
  197. package/src/analytics/errors.ts +18 -0
  198. package/src/analytics/eventInput.ts +168 -0
  199. package/src/analytics/persistence.ts +10 -0
  200. package/src/analytics/queryInput.ts +102 -0
  201. package/src/analytics/routes.ts +123 -0
  202. package/src/analytics/types.ts +44 -0
  203. package/src/apiKeys.integration.spec.ts +192 -0
  204. package/src/apiKeys.spec.ts +162 -0
  205. package/src/apiKeys.ts +220 -0
  206. package/src/createHotUpdaterCore.ts +164 -100
  207. package/src/db/bundleRows.spec.ts +164 -0
  208. package/src/db/bundleRows.ts +10 -140
  209. package/src/db/createBundleDiff.spec.ts +172 -127
  210. package/src/db/createBundleDiff.ts +91 -110
  211. package/src/db/databasePluginCore.spec.ts +62 -0
  212. package/src/db/databasePluginCore.testFixtures.ts +76 -0
  213. package/src/db/databasePluginCore.ts +185 -0
  214. package/src/db/databasePluginUpdateCheck.bench.ts +107 -0
  215. package/src/db/fixedMigrator.spec.ts +184 -42
  216. package/src/db/fixedMigrator.ts +64 -205
  217. package/src/db/fixedMigratorMongo.spec.ts +192 -0
  218. package/src/db/fixedMigratorMongo.ts +425 -0
  219. package/src/db/fixedMigratorShared.ts +36 -0
  220. package/src/db/hotUpdaterSchema.ts +0 -1
  221. package/src/db/index.spec.ts +333 -1025
  222. package/src/db/mongoMigrationExecution.ts +23 -0
  223. package/src/db/node-sqlite.d.ts +25 -0
  224. package/src/db/releaseCatalog.ts +148 -0
  225. package/src/db/schema/mongodb.ts +20 -6
  226. package/src/db/schema/sql.ts +52 -11
  227. package/src/db/schema/sqlOperations.ts +5 -4
  228. package/src/db/schemaGenerators.ts +107 -23
  229. package/src/db/schemaReadiness.ts +2 -1
  230. package/src/db/sqlMigrationExecution.spec.ts +65 -0
  231. package/src/db/sqlMigrationExecution.ts +108 -0
  232. package/src/db/types.spec.ts +55 -0
  233. package/src/db/types.ts +129 -65
  234. package/src/db/updateArtifacts.spec.ts +359 -0
  235. package/src/db/updateArtifacts.ts +290 -126
  236. package/src/handler-standalone.integration.spec.ts +401 -364
  237. package/src/handler.spec.ts +137 -582
  238. package/src/handler.testFixtures.ts +30 -0
  239. package/src/handler.ts +218 -616
  240. package/src/handlerAuth.integration.spec.ts +33 -0
  241. package/src/handlerBundleRoutes.ts +259 -0
  242. package/src/handlerErrors.ts +7 -0
  243. package/src/handlerManagement.spec.ts +262 -0
  244. package/src/handlerParameters.ts +105 -0
  245. package/src/handlerReleaseCatalogRoutes.ts +154 -0
  246. package/src/handlerReleaseManagementRoutes.ts +213 -0
  247. package/src/handlerTypes.ts +96 -0
  248. package/src/handlerVersionRoutes.ts +12 -0
  249. package/src/index.ts +22 -2
  250. package/src/node.spec.ts +79 -10
  251. package/src/node.ts +20 -17
  252. package/src/releaseCatalogRoutes.integration.spec.ts +238 -0
  253. package/src/runtime.spec.ts +126 -959
  254. package/src/runtime.testFixtures.ts +71 -0
  255. package/src/schema/dsl-all-versions.spec.ts +26 -17
  256. package/src/schema/dsl.ts +41 -2
  257. package/src/schema/index.ts +3 -9
  258. package/src/schema/types.ts +9 -3
  259. package/src/schema/v1_0_0.spec.ts +146 -0
  260. package/src/schema/v1_0_0.ts +465 -0
  261. package/src/storageAccess.spec.ts +127 -32
  262. package/src/storageAccess.ts +106 -55
  263. package/src/types/index.ts +2 -1
  264. package/dist/db/bundleRows.cjs +0 -82
  265. package/dist/db/bundleRows.d.cts +0 -33
  266. package/dist/db/bundleRows.d.mts +0 -33
  267. package/dist/db/bundleRows.mjs +0 -79
  268. package/dist/db/pluginCore.cjs +0 -242
  269. package/dist/db/pluginCore.mjs +0 -241
  270. package/dist/db/schema/sqlMigrations.cjs +0 -74
  271. package/dist/db/schema/sqlMigrations.mjs +0 -73
  272. package/dist/db/schemaEnhancements.cjs +0 -20
  273. package/dist/db/schemaEnhancements.mjs +0 -19
  274. package/dist/route.cjs +0 -7
  275. package/dist/route.mjs +0 -7
  276. package/dist/schema/v0_21_0.cjs +0 -38
  277. package/dist/schema/v0_21_0.mjs +0 -37
  278. package/dist/schema/v0_29_0.cjs +0 -45
  279. package/dist/schema/v0_29_0.mjs +0 -44
  280. package/dist/schema/v0_31_0.cjs +0 -82
  281. package/dist/schema/v0_31_0.mjs +0 -80
  282. package/src/db/pluginCore.spec.ts +0 -1095
  283. package/src/db/pluginCore.ts +0 -475
  284. package/src/db/pluginUpdateCheck.bench.ts +0 -254
  285. package/src/db/schema/sqlMigrations.ts +0 -219
  286. package/src/db/schemaEnhancements.ts +0 -55
  287. package/src/route.ts +0 -7
  288. package/src/schema/v0_21_0.ts +0 -52
  289. package/src/schema/v0_29_0.ts +0 -62
  290. package/src/schema/v0_31_0.spec.ts +0 -73
  291. package/src/schema/v0_31_0.ts +0 -120
@@ -1,107 +1,145 @@
1
1
  import { PGlite } from "@electric-sql/pglite";
2
- import type { Bundle } from "@hot-updater/core";
3
2
  import { Kysely } from "kysely";
4
3
  import { PGliteDialect } from "kysely-pglite-dialect";
5
- import { afterEach, describe, expect, it } from "vitest";
4
+ import { describe, expect, it } from "vitest";
6
5
 
7
- import { createHotUpdater } from "../index";
8
6
  import {
9
- HOT_UPDATER_SCHEMA_VERSION,
10
- HOT_UPDATER_SETTINGS_TABLE,
11
- } from "../schema/types";
7
+ createBundlePatchRowFixture,
8
+ createBundleRowFixture,
9
+ createChannelRowFixture,
10
+ } from "../../../test-utils/src/databaseTestFixtures";
11
+ import { setupDatabasePluginTestSuite } from "../../../test-utils/src/setupDatabasePluginTestSuite";
12
+ import type { DatabaseAdapterWithCapabilities } from "../db/types";
13
+ import {
14
+ DATABASE_PLUGIN_TEST_RESET_SQL,
15
+ DATABASE_PLUGIN_TEST_SCHEMA_SQL,
16
+ } from "./databasePluginTestDatabase";
12
17
  import { kyselyAdapter } from "./kysely";
13
18
 
14
- const sqliteJsonBundle: Bundle = {
15
- id: "00000000-0000-0000-0000-000000000901",
16
- platform: "ios",
17
- shouldForceUpdate: false,
18
- enabled: true,
19
- fileHash: "sqlite-json-hash",
20
- gitCommitHash: null,
21
- message: "sqlite json bundle",
22
- channel: "production",
23
- storageUri: "s3://bucket/sqlite-json.zip",
24
- targetAppVersion: "1.0.0",
25
- fingerprintHash: null,
26
- metadata: { app_version: "1.0.0" },
27
- targetCohorts: ["17", "qa-group"],
19
+ class KyselyTestStateError extends Error {
20
+ readonly name = "KyselyTestStateError";
21
+ }
22
+
23
+ let client: PGlite | undefined;
24
+ let database: Kysely<object> | undefined;
25
+
26
+ const getClient = (): PGlite => {
27
+ if (client === undefined) throw new KyselyTestStateError();
28
+ return client;
28
29
  };
29
30
 
30
- describe("kyselyAdapter sqlite provider", () => {
31
- const databases: PGlite[] = [];
32
- const kyselyInstances: Kysely<object>[] = [];
31
+ const getDatabase = (): Kysely<object> => {
32
+ if (database === undefined) throw new KyselyTestStateError();
33
+ return database;
34
+ };
33
35
 
34
- afterEach(async () => {
35
- for (const kysely of kyselyInstances.splice(0)) {
36
- await kysely.destroy();
37
- }
38
- for (const db of databases.splice(0)) {
39
- await db.close();
40
- }
41
- });
36
+ setupDatabasePluginTestSuite({
37
+ name: "kyselyAdapter PostgreSQL",
38
+ migrate: async () => {
39
+ client = new PGlite();
40
+ database = new Kysely<object>({ dialect: new PGliteDialect(client) });
41
+ await client.exec(DATABASE_PLUGIN_TEST_SCHEMA_SQL);
42
+ },
43
+ createPlugin: (): DatabaseAdapterWithCapabilities =>
44
+ kyselyAdapter({ db: getDatabase(), provider: "postgresql" }),
45
+ reset: async () => {
46
+ await getClient().exec(DATABASE_PLUGIN_TEST_RESET_SQL);
47
+ },
48
+ dispose: async () => {
49
+ await getDatabase().destroy();
50
+ await getClient().close();
51
+ database = undefined;
52
+ client = undefined;
53
+ },
54
+ });
42
55
 
43
- it("stores bundle JSON columns as text and round-trips them", async () => {
44
- const db = new PGlite();
45
- databases.push(db);
46
- const kysely = new Kysely<object>({ dialect: new PGliteDialect(db) });
47
- kyselyInstances.push(kysely);
48
- await db.exec(`
49
- create table bundles (
50
- id text primary key,
51
- platform text not null,
52
- should_force_update boolean not null,
53
- enabled boolean not null,
54
- file_hash text not null,
55
- git_commit_hash text,
56
- message text,
57
- channel text not null,
58
- storage_uri text not null,
59
- target_app_version text,
60
- fingerprint_hash text,
61
- metadata text not null,
62
- manifest_storage_uri text,
63
- manifest_file_hash text,
64
- asset_base_storage_uri text,
65
- rollout_cohort_count integer not null,
66
- target_cohorts text
67
- );
68
- create table bundle_patches (
69
- id text primary key,
70
- bundle_id text not null,
71
- base_bundle_id text not null,
72
- base_file_hash text not null,
73
- patch_file_hash text not null,
74
- patch_storage_uri text not null,
75
- order_index integer not null
76
- );
77
- create table ${HOT_UPDATER_SETTINGS_TABLE} (
78
- key text primary key,
79
- value text not null
80
- );
81
- insert into ${HOT_UPDATER_SETTINGS_TABLE} (key, value)
82
- values ('version', '${HOT_UPDATER_SCHEMA_VERSION}');
83
- `);
84
- const hotUpdater = createHotUpdater({
85
- database: kyselyAdapter({
86
- db: kysely,
87
- provider: "sqlite",
88
- }),
56
+ describe("kyselyAdapter SQLite JSON storage", () => {
57
+ it("round-trips JSON values through text columns", async () => {
58
+ const sqliteClient = new PGlite();
59
+ const sqliteDatabase = new Kysely<object>({
60
+ dialect: new PGliteDialect(sqliteClient),
89
61
  });
62
+ await sqliteClient.exec(
63
+ DATABASE_PLUGIN_TEST_SCHEMA_SQL.replace(
64
+ "metadata jsonb not null default '{}'::jsonb",
65
+ "metadata text not null",
66
+ ),
67
+ );
68
+ const plugin = kyselyAdapter({ db: sqliteDatabase, provider: "sqlite" });
69
+ const row = {
70
+ ...createBundleRowFixture("901"),
71
+ metadata: { app_version: "1.0.0" },
72
+ };
90
73
 
91
- await hotUpdater.insertBundle(sqliteJsonBundle);
92
- const stored = await db.query<{
74
+ await plugin.commit({
75
+ changes: [{ model: "bundles", operation: "insert", row }],
76
+ });
77
+ const stored = await sqliteClient.query<{
93
78
  metadata: string;
94
- target_cohorts: string;
95
- }>("select metadata, target_cohorts from bundles where id = $1", [
96
- sqliteJsonBundle.id,
97
- ]);
98
- const restored = await hotUpdater.getBundleById(sqliteJsonBundle.id);
79
+ }>("select metadata from bundles where id = $1", [row.id]);
99
80
 
100
81
  expect(stored.rows[0]).toEqual({
101
- metadata: JSON.stringify({ app_version: "1.0.0" }),
102
- target_cohorts: JSON.stringify(["17", "qa-group"]),
82
+ metadata: JSON.stringify(row.metadata),
83
+ });
84
+ await expect(plugin.models.bundles.findById(row.id)).resolves.toEqual(row);
85
+ await sqliteDatabase.destroy();
86
+ await sqliteClient.close();
87
+ });
88
+ });
89
+
90
+ describe("kyselyAdapter soft relations", () => {
91
+ it("rejects an orphan patch and rolls back its owner row", async () => {
92
+ const softClient = new PGlite();
93
+ const softDatabase = new Kysely<object>({
94
+ dialect: new PGliteDialect(softClient),
95
+ });
96
+ await softClient.exec(
97
+ DATABASE_PLUGIN_TEST_SCHEMA_SQL.replaceAll(
98
+ " references bundles(id) on delete cascade",
99
+ "",
100
+ ),
101
+ );
102
+ const plugin = kyselyAdapter({
103
+ db: softDatabase,
104
+ provider: "postgresql",
105
+ relationMode: "fumadb",
103
106
  });
104
- expect(restored?.metadata).toEqual({ app_version: "1.0.0" });
105
- expect(restored?.targetCohorts).toEqual(["17", "qa-group"]);
107
+ const owner = createBundleRowFixture("952");
108
+ const patch = createBundlePatchRowFixture(
109
+ "missing-base",
110
+ owner.id,
111
+ "missing-base",
112
+ );
113
+ const channel = createChannelRowFixture();
114
+
115
+ try {
116
+ await expect(
117
+ plugin.commit({
118
+ changes: [
119
+ {
120
+ model: "channels",
121
+ operation: "insert",
122
+ row: channel,
123
+ onConflict: "ignore",
124
+ },
125
+ { model: "bundles", operation: "insert", row: owner },
126
+ {
127
+ model: "bundlePatches",
128
+ operation: "insert",
129
+ row: patch,
130
+ },
131
+ ],
132
+ }),
133
+ ).rejects.toThrow("bundle_patches.base_bundle_id.foreign-key");
134
+ await expect(
135
+ plugin.models.bundles.findById(owner.id),
136
+ ).resolves.toBeNull();
137
+ await expect(plugin.models.channels.list({})).resolves.toEqual({
138
+ channels: [],
139
+ });
140
+ } finally {
141
+ await softDatabase.destroy();
142
+ await softClient.close();
143
+ }
106
144
  });
107
145
  });
@@ -1,359 +1,101 @@
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 { Kysely, Transaction } from "kysely";
3
+ createDatabasePluginAdapter,
4
+ type DatabasePluginImplementation,
5
+ } from "@hot-updater/plugin-core/internal";
6
+ import type { Kysely } from "kysely";
14
7
 
15
- import {
16
- bundleToPatchRows,
17
- bundleToRow,
18
- type BundlePatchRow,
19
- type BundleRow,
20
- rowToBundle,
21
- } from "../db/bundleRows";
22
8
  import { createKyselyMigrator } from "../db/fixedMigrator";
23
9
  import type {
24
- DatabasePluginFactory,
10
+ DatabaseAdapterWithCapabilities,
25
11
  ORMSQLProvider,
26
12
  RelationMode,
27
13
  } from "../db/types";
14
+ import { createKyselyCrud } from "./kyselyCrud";
28
15
 
29
16
  type KyselySQLProvider = Exclude<ORMSQLProvider, "mssql">;
30
17
 
31
18
  export type { RelationMode, KyselySQLProvider as SQLProvider };
32
19
 
33
- interface Database {
34
- readonly bundles: BundleRow;
35
- readonly bundle_patches: BundlePatchRow;
36
- readonly private_hot_updater_settings: {
37
- readonly key: string;
38
- readonly value: string;
39
- };
40
- }
41
-
42
- export interface KyselyAdapterConfig<TDatabase extends object = Database> {
20
+ export interface KyselyAdapterConfig<TDatabase extends object = object> {
43
21
  readonly db: Kysely<TDatabase>;
44
22
  readonly provider: KyselySQLProvider;
45
23
  readonly relationMode?: RelationMode;
46
24
  }
47
25
 
48
- const assertKyselySQLProvider: (
49
- provider: ORMSQLProvider,
50
- ) => asserts provider is KyselySQLProvider = (provider) => {
51
- if (provider === "mssql") {
52
- throw new Error("Kysely adapter does not support provider: mssql.");
53
- }
54
- };
55
-
56
- const applyWhere = <T extends object>(
57
- query: T,
58
- where: DatabaseBundleQueryWhere | undefined,
59
- ): T => {
60
- let next = query as {
61
- where: (column: string, op: string, value?: unknown) => unknown;
62
- };
63
- if (where?.channel !== undefined)
64
- next = next.where("channel", "=", where.channel) as typeof next;
65
- if (where?.platform !== undefined)
66
- next = next.where("platform", "=", where.platform) as typeof next;
67
- if (where?.enabled !== undefined)
68
- next = next.where("enabled", "=", where.enabled) as typeof next;
69
- if (where?.fingerprintHash !== undefined) {
70
- next =
71
- where.fingerprintHash === null
72
- ? (next.where("fingerprint_hash", "is", null) as typeof next)
73
- : (next.where(
74
- "fingerprint_hash",
75
- "=",
76
- where.fingerprintHash,
77
- ) as typeof next);
78
- }
79
- if (where?.targetAppVersion !== undefined) {
80
- next =
81
- where.targetAppVersion === null
82
- ? (next.where("target_app_version", "is", null) as typeof next)
83
- : (next.where(
84
- "target_app_version",
85
- "=",
86
- where.targetAppVersion,
87
- ) as typeof next);
88
- }
89
- if (where?.targetAppVersionIn) {
90
- next = next.where(
91
- "target_app_version",
92
- "in",
93
- where.targetAppVersionIn,
94
- ) as typeof next;
95
- }
96
- if (where?.targetAppVersionNotNull) {
97
- next = next.where("target_app_version", "is not", null) as typeof next;
98
- }
99
- if (where?.id?.eq) next = next.where("id", "=", where.id.eq) as typeof next;
100
- if (where?.id?.gt) next = next.where("id", ">", where.id.gt) as typeof next;
101
- if (where?.id?.gte)
102
- next = next.where("id", ">=", where.id.gte) as typeof next;
103
- if (where?.id?.lt) next = next.where("id", "<", where.id.lt) as typeof next;
104
- if (where?.id?.lte)
105
- next = next.where("id", "<=", where.id.lte) as typeof next;
106
- if (where?.id?.in) next = next.where("id", "in", where.id.in) as typeof next;
107
- return next as T;
108
- };
109
-
110
- const hasEmptySetFilter = (
111
- where: DatabaseBundleQueryWhere | undefined,
112
- ): boolean =>
113
- where?.targetAppVersionIn?.length === 0 || where?.id?.in?.length === 0;
114
-
115
- const toProviderBundleRow = (
116
- row: BundleRow,
117
- provider: KyselySQLProvider,
118
- ): BundleRow => {
119
- if (provider !== "mysql" && provider !== "sqlite") return row;
26
+ const createImplementation = <TDatabase extends object>(
27
+ config: KyselyAdapterConfig<TDatabase>,
28
+ ): DatabasePluginImplementation => {
29
+ const db = config.db;
30
+ const relationMode = config.relationMode ?? "foreign-keys";
31
+ const crud = createKyselyCrud(db, config.provider, relationMode);
120
32
  return {
121
- ...row,
122
- metadata: JSON.stringify(row.metadata ?? {}),
123
- target_cohorts:
124
- row.target_cohorts === null || row.target_cohorts === undefined
125
- ? null
126
- : JSON.stringify(row.target_cohorts),
33
+ ...crud,
34
+ deleteChannel: (input) =>
35
+ db
36
+ .transaction()
37
+ .execute((transaction) =>
38
+ createKyselyCrud(
39
+ transaction,
40
+ config.provider,
41
+ relationMode,
42
+ ).deleteChannel(input),
43
+ ),
44
+ create: (input) =>
45
+ db
46
+ .transaction()
47
+ .execute((transaction) =>
48
+ createKyselyCrud(transaction, config.provider, relationMode).create(
49
+ input,
50
+ ),
51
+ ),
52
+ update: (input) =>
53
+ db
54
+ .transaction()
55
+ .execute((transaction) =>
56
+ createKyselyCrud(transaction, config.provider, relationMode).update(
57
+ input,
58
+ ),
59
+ ),
60
+ delete: (input) =>
61
+ db
62
+ .transaction()
63
+ .execute((transaction) =>
64
+ createKyselyCrud(transaction, config.provider, relationMode).delete(
65
+ input,
66
+ ),
67
+ ),
68
+ transaction: (callback) =>
69
+ db
70
+ .transaction()
71
+ .execute((transaction) =>
72
+ callback(
73
+ createKyselyCrud(transaction, config.provider, relationMode),
74
+ ),
75
+ ),
127
76
  };
128
77
  };
129
78
 
130
- const createKyselyPlugin = createDatabasePlugin<KyselyAdapterConfig<Database>>({
131
- name: "kysely",
132
- factory: ({ db, provider }) => {
133
- const fetchPatchMap = async (bundleIds: readonly string[]) => {
134
- const patchMap = new Map<string, BundlePatchRow[]>();
135
- if (bundleIds.length === 0) return patchMap;
136
- const rows = await db
137
- .selectFrom("bundle_patches")
138
- .selectAll()
139
- .where("bundle_id", "in", [...bundleIds])
140
- .orderBy("order_index", "asc")
141
- .execute();
142
- for (const row of rows) {
143
- const current = patchMap.get(row.bundle_id) ?? [];
144
- current.push(row);
145
- patchMap.set(row.bundle_id, current);
146
- }
147
- return patchMap;
148
- };
149
- const mapRowsToBundles = async (
150
- rows: readonly BundleRow[],
151
- ): Promise<Bundle[]> => {
152
- const patchMap = await fetchPatchMap(rows.map((row) => row.id));
153
- return rows.map((row) => rowToBundle(row, patchMap.get(row.id) ?? []));
154
- };
155
-
156
- const upsertBundle = async (
157
- executor: Kysely<Database> | Transaction<Database>,
158
- bundle: Bundle,
159
- ) => {
160
- const row = toProviderBundleRow(bundleToRow(bundle), provider);
161
- const { id: _id, ...updateRow } = row;
162
- if (provider === "mysql") {
163
- await executor
164
- .insertInto("bundles")
165
- .values(row)
166
- .onDuplicateKeyUpdate(updateRow)
167
- .execute();
168
- } else {
169
- await executor
170
- .insertInto("bundles")
171
- .values(row)
172
- .onConflict((oc) => oc.column("id").doUpdateSet(updateRow))
173
- .execute();
174
- }
175
- await executor
176
- .deleteFrom("bundle_patches")
177
- .where("bundle_id", "=", bundle.id)
178
- .execute();
179
- const patches = bundleToPatchRows(bundle);
180
- if (patches.length > 0) {
181
- await executor.insertInto("bundle_patches").values(patches).execute();
182
- }
183
- };
184
-
185
- return {
186
- async getBundleById(bundleId) {
187
- const row = await db
188
- .selectFrom("bundles")
189
- .selectAll()
190
- .where("id", "=", bundleId)
191
- .executeTakeFirst();
192
- if (!row) return null;
193
- const patchMap = await fetchPatchMap([bundleId]);
194
- return rowToBundle(row, patchMap.get(bundleId) ?? []);
195
- },
196
- async getBundles(
197
- options: DatabaseBundleQueryOptions & { offset?: number },
198
- ) {
199
- const offset = options.offset ?? 0;
200
- if (hasEmptySetFilter(options.where)) {
201
- return {
202
- data: [],
203
- pagination: calculatePagination(0, {
204
- limit: options.limit,
205
- offset,
206
- }),
207
- };
208
- }
209
- const orderBy = options.orderBy ?? { field: "id", direction: "desc" };
210
- const countRow = await applyWhere(
211
- db.selectFrom("bundles"),
212
- options.where,
213
- )
214
- .select(db.fn.count<number>("id").as("total"))
215
- .executeTakeFirst();
216
- const total = Number(countRow?.total ?? 0);
217
- const rows = await applyWhere(
218
- db.selectFrom("bundles").selectAll(),
219
- options.where,
220
- )
221
- .orderBy("id", orderBy.direction)
222
- .limit(options.limit)
223
- .offset(offset)
224
- .execute();
225
- const patchMap = await fetchPatchMap(rows.map((row) => row.id));
226
- return {
227
- data: rows.map((row) => rowToBundle(row, patchMap.get(row.id) ?? [])),
228
- pagination: calculatePagination(total, {
229
- limit: options.limit,
230
- offset,
231
- }),
232
- };
233
- },
234
- async getUpdateInfo(args, context) {
235
- if (args._updateStrategy === "appVersion") {
236
- const channel = args.channel ?? "production";
237
- const minBundleId = args.minBundleId ?? NIL_UUID;
238
- const rows = await db
239
- .selectFrom("bundles")
240
- .select("target_app_version")
241
- .where("enabled", "=", true)
242
- .where("platform", "=", args.platform)
243
- .where("channel", "=", channel)
244
- .where("id", ">=", minBundleId)
245
- .where("target_app_version", "is not", null)
246
- .execute();
247
-
248
- const targetAppVersions = Array.from(
249
- new Set(
250
- rows
251
- .map((row) => row.target_app_version)
252
- .filter(
253
- (value): value is string =>
254
- typeof value === "string" && value.length > 0,
255
- ),
256
- ),
257
- );
258
- const compatibleAppVersions = filterCompatibleAppVersions(
259
- targetAppVersions,
260
- args.appVersion,
261
- );
262
- const bundles =
263
- compatibleAppVersions.length > 0
264
- ? await db
265
- .selectFrom("bundles")
266
- .selectAll()
267
- .where("enabled", "=", true)
268
- .where("platform", "=", args.platform)
269
- .where("channel", "=", channel)
270
- .where("id", ">=", minBundleId)
271
- .where("target_app_version", "in", compatibleAppVersions)
272
- .orderBy("id", "desc")
273
- .execute()
274
- .then(mapRowsToBundles)
275
- : [];
276
-
277
- return resolveUpdateInfoFromBundles({
278
- args: { ...args, channel, minBundleId },
279
- bundles,
280
- context,
281
- });
282
- }
283
-
284
- const channel = args.channel ?? "production";
285
- const minBundleId = args.minBundleId ?? NIL_UUID;
286
- const rows = await db
287
- .selectFrom("bundles")
288
- .selectAll()
289
- .where("enabled", "=", true)
290
- .where("platform", "=", args.platform)
291
- .where("channel", "=", channel)
292
- .where("id", ">=", minBundleId)
293
- .where("fingerprint_hash", "=", args.fingerprintHash)
294
- .orderBy("id", "desc")
295
- .execute();
296
-
297
- return resolveUpdateInfoFromBundles({
298
- args: { ...args, channel, minBundleId },
299
- bundles: await mapRowsToBundles(rows),
300
- context,
301
- });
302
- },
303
- async getChannels() {
304
- const rows = await db
305
- .selectFrom("bundles")
306
- .select("channel")
307
- .orderBy("channel", "asc")
308
- .execute();
309
- return Array.from(new Set(rows.map((row) => row.channel)));
310
- },
311
- async commitBundle({ changedSets }) {
312
- await db.transaction().execute(async (tx) => {
313
- for (const change of changedSets) {
314
- if (change.operation === "delete") {
315
- await tx
316
- .deleteFrom("bundle_patches")
317
- .where("bundle_id", "=", change.data.id)
318
- .execute();
319
- await tx
320
- .deleteFrom("bundle_patches")
321
- .where("base_bundle_id", "=", change.data.id)
322
- .execute();
323
- await tx
324
- .deleteFrom("bundles")
325
- .where("id", "=", change.data.id)
326
- .execute();
327
- continue;
328
- }
329
- await upsertBundle(tx, change.data);
330
- }
331
- });
332
- },
333
- };
334
- },
335
- });
336
-
337
79
  export const kyselyAdapter = <TDatabase extends object>(
338
80
  config: KyselyAdapterConfig<TDatabase>,
339
- ): DatabasePluginFactory => {
340
- assertKyselySQLProvider(config.provider);
341
- return Object.assign(
342
- createKyselyPlugin(config as unknown as KyselyAdapterConfig<Database>),
343
- {
344
- adapterName: "kysely",
345
- provider: config.provider,
346
- createMigrator: () =>
347
- createKyselyMigrator({
348
- db: config.db as unknown as Kysely<{
349
- private_hot_updater_settings: {
350
- key: string;
351
- value: string;
352
- };
353
- }>,
354
- provider: config.provider,
355
- relationMode: config.relationMode,
356
- }),
357
- },
81
+ ): DatabaseAdapterWithCapabilities => {
82
+ const adapter = createDatabasePluginAdapter(
83
+ "kysely",
84
+ createImplementation<TDatabase>(config),
358
85
  );
86
+ const plugin = createDatabasePlugin({
87
+ name: "kysely",
88
+ models: adapter.models,
89
+ commit: adapter.commit,
90
+ });
91
+ return Object.assign(plugin, {
92
+ adapterName: "kysely",
93
+ provider: config.provider,
94
+ createMigrator: () =>
95
+ createKyselyMigrator({
96
+ db: config.db,
97
+ provider: config.provider,
98
+ relationMode: config.relationMode,
99
+ }),
100
+ });
359
101
  };