@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,25 @@
1
+ import { DatabaseRowReferencedError } from "@hot-updater/plugin-core/internal";
2
+
3
+ const readErrorCode = (error: unknown): string | undefined => {
4
+ if (typeof error !== "object" || error === null) return undefined;
5
+ const code = Reflect.get(error, "code");
6
+ return typeof code === "string" ? code : undefined;
7
+ };
8
+
9
+ export const isChannelDeleteReferencedError = (error: unknown): boolean =>
10
+ error instanceof DatabaseRowReferencedError ||
11
+ [
12
+ "23503",
13
+ "SQLITE_CONSTRAINT_FOREIGNKEY",
14
+ "SQLITE_CONSTRAINT",
15
+ "ER_ROW_IS_REFERENCED",
16
+ "ER_ROW_IS_REFERENCED_2",
17
+ "P2003",
18
+ ].includes(readErrorCode(error) ?? "");
19
+
20
+ export const translateChannelDeleteError = (error: unknown): never => {
21
+ if (isChannelDeleteReferencedError(error)) {
22
+ throw new DatabaseRowReferencedError();
23
+ }
24
+ throw error;
25
+ };
@@ -0,0 +1,106 @@
1
+ export const DATABASE_PLUGIN_TEST_SCHEMA_SQL = `
2
+ create table channels (
3
+ id text primary key,
4
+ name text not null unique
5
+ );
6
+ create table bundles (
7
+ id text primary key,
8
+ platform text not null,
9
+ file_hash text not null,
10
+ git_commit_hash text,
11
+ storage_uri text not null,
12
+ archive_byte_size double precision not null check (
13
+ archive_byte_size >= 0 and archive_byte_size <= 9007199254740991
14
+ ),
15
+ metadata jsonb not null default '{}'::jsonb,
16
+ manifest_storage_uri text,
17
+ manifest_file_hash text,
18
+ asset_base_storage_uri text
19
+ );
20
+ create table bundle_patches (
21
+ id varchar(255) primary key,
22
+ bundle_id text not null references bundles(id) on delete cascade,
23
+ base_bundle_id text not null references bundles(id) on delete cascade,
24
+ base_file_hash text not null,
25
+ patch_file_hash text not null,
26
+ patch_storage_uri text not null,
27
+ byte_size double precision not null check (
28
+ byte_size >= 0 and byte_size <= 9007199254740991
29
+ ),
30
+ order_index integer not null default 0
31
+ );
32
+ create table releases (
33
+ id text primary key,
34
+ revision integer not null,
35
+ scope_key text not null,
36
+ channel_id text not null references channels(id),
37
+ platform text not null,
38
+ kind text not null,
39
+ bundle_id text references bundles(id),
40
+ strategy text not null,
41
+ target_app_version text,
42
+ fingerprint_hash text,
43
+ enabled boolean not null,
44
+ should_force_update boolean not null,
45
+ message text,
46
+ rollout_cohort_count integer not null default 1000,
47
+ target_cohorts jsonb not null default '[]'::jsonb,
48
+ operation text not null,
49
+ source_release_id text references releases(id) on delete set null,
50
+ created_at_ms integer not null,
51
+ updated_at_ms integer not null
52
+ );
53
+ create table release_catalogs (
54
+ scope_key text primary key,
55
+ catalog_id text not null,
56
+ strategy text not null,
57
+ channel_id text not null references channels(id),
58
+ channel_key text not null,
59
+ platform text not null,
60
+ fingerprint_hash text,
61
+ generation integer not null,
62
+ payload text not null,
63
+ catalog_hash text not null,
64
+ byte_size integer not null,
65
+ is_tombstone boolean not null,
66
+ updated_at_ms integer not null
67
+ );
68
+ create table bundle_events (
69
+ id text primary key,
70
+ type text not null,
71
+ install_id text not null,
72
+ user_id text,
73
+ username text,
74
+ from_release_id text,
75
+ from_bundle_id text,
76
+ to_release_id text,
77
+ to_bundle_id text not null,
78
+ platform text not null,
79
+ app_version text not null,
80
+ channel text not null,
81
+ cohort text not null,
82
+ update_strategy text,
83
+ fingerprint_hash text,
84
+ sdk_version text,
85
+ received_at_ms integer not null
86
+ );
87
+ create table api_keys (
88
+ id text primary key,
89
+ hash text not null unique,
90
+ name text not null,
91
+ prefix text not null,
92
+ role text not null,
93
+ created_at_ms integer not null,
94
+ revoked_at_ms integer
95
+ );
96
+ `;
97
+
98
+ export const DATABASE_PLUGIN_TEST_RESET_SQL = `
99
+ delete from bundle_events;
100
+ delete from api_keys;
101
+ delete from bundle_patches;
102
+ delete from release_catalogs;
103
+ delete from releases;
104
+ delete from bundles;
105
+ delete from channels;
106
+ `;
@@ -0,0 +1,29 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { createBundleRowFixture } from "../../../test-utils/src/databaseTestFixtures";
4
+ import { fromStoredBundleRow } from "./databasePluginUtils";
5
+
6
+ describe("fromStoredBundleRow", () => {
7
+ it("parses JSON-object metadata from SQL text storage", () => {
8
+ const row = fromStoredBundleRow({
9
+ ...createBundleRowFixture("valid-metadata"),
10
+ metadata: '{"release":{"flags":[true,null,3]}}',
11
+ });
12
+
13
+ expect(row.metadata).toEqual({
14
+ release: { flags: [true, null, 3] },
15
+ });
16
+ });
17
+
18
+ it.each(["null", "[]", "1", '"metadata"', "not-json"])(
19
+ "rejects non-object SQL metadata %s",
20
+ (metadata) => {
21
+ expect(() =>
22
+ fromStoredBundleRow({
23
+ ...createBundleRowFixture("invalid-metadata"),
24
+ metadata,
25
+ }),
26
+ ).toThrow("Invalid metadata");
27
+ },
28
+ );
29
+ });
@@ -0,0 +1,214 @@
1
+ import type {
2
+ BundleRow,
3
+ ReleaseCatalogRow,
4
+ ReleaseRow,
5
+ ReleaseRowUpdate,
6
+ } from "@hot-updater/plugin-core";
7
+ import { isDatabaseMetadataObject } from "@hot-updater/plugin-core";
8
+ import type {
9
+ DatabaseModel,
10
+ DatabaseOrderBy,
11
+ DatabaseSortBy,
12
+ } from "@hot-updater/plugin-core/internal";
13
+
14
+ import type { ORMSQLProvider } from "../db/types";
15
+
16
+ export type StoredBundleRow = Omit<BundleRow, "metadata"> & {
17
+ readonly metadata: unknown;
18
+ };
19
+
20
+ export type StoredReleaseRow = Omit<ReleaseRow, "target_cohorts"> & {
21
+ readonly target_cohorts: unknown;
22
+ };
23
+
24
+ export type StoredReleaseCatalogRow = Omit<
25
+ ReleaseCatalogRow,
26
+ "is_tombstone"
27
+ > & {
28
+ readonly is_tombstone: unknown;
29
+ };
30
+
31
+ type AnyDatabaseSortBy = {
32
+ readonly [TModel in DatabaseModel]: DatabaseSortBy<TModel>;
33
+ }[DatabaseModel];
34
+ type AnyDatabaseOrderBy = {
35
+ readonly [TModel in DatabaseModel]: DatabaseOrderBy<TModel>;
36
+ }[DatabaseModel];
37
+ type AnyDatabaseOrderClause = AnyDatabaseSortBy;
38
+
39
+ class StoredBundleRowError extends Error {
40
+ readonly name = "StoredBundleRowError";
41
+ }
42
+
43
+ const parseStoredBoolean = (value: unknown, field: string): boolean => {
44
+ if (value === true || value === 1) return true;
45
+ if (value === false || value === 0) return false;
46
+ throw new StoredBundleRowError(`Invalid boolean value for ${field}.`);
47
+ };
48
+
49
+ const parseJson = (value: string): unknown => {
50
+ try {
51
+ return JSON.parse(value);
52
+ } catch (error) {
53
+ if (error instanceof SyntaxError) return value;
54
+ throw error;
55
+ }
56
+ };
57
+
58
+ const parseTargetCohorts = (value: unknown): readonly string[] | null => {
59
+ if (value === null) return null;
60
+ const parsed = typeof value === "string" ? parseJson(value) : value;
61
+ if (
62
+ !Array.isArray(parsed) ||
63
+ !parsed.every((item) => typeof item === "string")
64
+ ) {
65
+ throw new StoredBundleRowError("Invalid target_cohorts field.");
66
+ }
67
+ return parsed;
68
+ };
69
+
70
+ const parseMetadata = (value: unknown) => {
71
+ const parsed = typeof value === "string" ? parseJson(value) : value;
72
+ if (!isDatabaseMetadataObject(parsed)) {
73
+ throw new StoredBundleRowError("Invalid metadata field.");
74
+ }
75
+ return parsed;
76
+ };
77
+
78
+ const compareOrderValues = (left: unknown, right: unknown): number => {
79
+ if (typeof left === "number" && typeof right === "number") {
80
+ return left - right;
81
+ }
82
+ if (typeof left === "string" && typeof right === "string") {
83
+ return left.localeCompare(right);
84
+ }
85
+ if (typeof left === "boolean" && typeof right === "boolean") {
86
+ return Number(left) - Number(right);
87
+ }
88
+ if (left === right) {
89
+ return 0;
90
+ }
91
+ return String(left).localeCompare(String(right));
92
+ };
93
+
94
+ const normalizeOrderBy = (
95
+ orderBy: AnyDatabaseOrderBy | readonly AnyDatabaseSortBy[] | undefined,
96
+ ): readonly AnyDatabaseOrderClause[] => {
97
+ if (orderBy === undefined) {
98
+ return [];
99
+ }
100
+ return (
101
+ Array.isArray(orderBy) ? orderBy : [orderBy]
102
+ ) as readonly AnyDatabaseOrderClause[];
103
+ };
104
+
105
+ export const hasNullOrderOverrides = (
106
+ orderBy: AnyDatabaseOrderBy | readonly AnyDatabaseSortBy[] | undefined,
107
+ ): boolean =>
108
+ normalizeOrderBy(orderBy).some((clause) => clause.nulls !== undefined);
109
+
110
+ export const sortRowsByOrder = <TRow extends object>(
111
+ rows: readonly TRow[],
112
+ orderBy: AnyDatabaseOrderBy | readonly AnyDatabaseSortBy[] | undefined,
113
+ ): TRow[] => {
114
+ const clauses = normalizeOrderBy(orderBy);
115
+ if (clauses.length === 0) {
116
+ return [...rows];
117
+ }
118
+
119
+ return [...rows].toSorted((left, right) => {
120
+ for (const clause of clauses) {
121
+ const leftValue = Reflect.get(left, clause.field);
122
+ const rightValue = Reflect.get(right, clause.field);
123
+
124
+ if (leftValue == null || rightValue == null) {
125
+ if (leftValue == null && rightValue == null) {
126
+ continue;
127
+ }
128
+ const nulls =
129
+ clause.nulls ?? (clause.direction === "asc" ? "last" : "first");
130
+ const order = leftValue == null ? -1 : 1;
131
+ return nulls === "first" ? order : -order;
132
+ }
133
+
134
+ const result = compareOrderValues(leftValue, rightValue);
135
+ if (result !== 0) {
136
+ return clause.direction === "desc" ? -result : result;
137
+ }
138
+ }
139
+ return 0;
140
+ });
141
+ };
142
+
143
+ export const fromStoredBundleRow = (row: StoredBundleRow): BundleRow => ({
144
+ ...row,
145
+ metadata: parseMetadata(row.metadata),
146
+ });
147
+
148
+ export const toStoredBundleRow = (
149
+ row: BundleRow,
150
+ provider: ORMSQLProvider,
151
+ ): StoredBundleRow => {
152
+ if (provider !== "mysql" && provider !== "sqlite") return row;
153
+ return {
154
+ ...row,
155
+ metadata: JSON.stringify(row.metadata),
156
+ };
157
+ };
158
+
159
+ export const toStoredBundleUpdate = (
160
+ update: Partial<Omit<BundleRow, "id">>,
161
+ provider: ORMSQLProvider,
162
+ ): Partial<Omit<StoredBundleRow, "id">> => {
163
+ if (provider !== "mysql" && provider !== "sqlite") return update;
164
+ return {
165
+ ...update,
166
+ ...("metadata" in update
167
+ ? { metadata: JSON.stringify(update.metadata) }
168
+ : {}),
169
+ };
170
+ };
171
+
172
+ export const fromStoredReleaseRow = (row: StoredReleaseRow): ReleaseRow => ({
173
+ ...row,
174
+ enabled: parseStoredBoolean(row.enabled, "enabled"),
175
+ should_force_update: parseStoredBoolean(
176
+ row.should_force_update,
177
+ "should_force_update",
178
+ ),
179
+ target_cohorts: parseTargetCohorts(row.target_cohorts) ?? [],
180
+ });
181
+
182
+ export const toStoredReleaseRow = (
183
+ row: ReleaseRow,
184
+ provider: ORMSQLProvider,
185
+ ): StoredReleaseRow => {
186
+ if (provider !== "mysql" && provider !== "sqlite") return row;
187
+ return { ...row, target_cohorts: JSON.stringify(row.target_cohorts) };
188
+ };
189
+
190
+ export const toStoredReleaseUpdate = (
191
+ update: ReleaseRowUpdate,
192
+ provider: ORMSQLProvider,
193
+ ): Partial<Omit<StoredReleaseRow, "id">> => {
194
+ if (provider !== "mysql" && provider !== "sqlite") return update;
195
+ return {
196
+ ...update,
197
+ ...(update.target_cohorts === undefined
198
+ ? {}
199
+ : { target_cohorts: JSON.stringify(update.target_cohorts) }),
200
+ };
201
+ };
202
+
203
+ export const fromStoredReleaseCatalogRow = (
204
+ row: StoredReleaseCatalogRow,
205
+ ): ReleaseCatalogRow => ({
206
+ ...row,
207
+ is_tombstone: parseStoredBoolean(row.is_tombstone, "is_tombstone"),
208
+ });
209
+
210
+ export const escapeLikePattern = (value: string): string =>
211
+ value.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");
212
+
213
+ export const escapeGlobPattern = (value: string): string =>
214
+ value.replaceAll("[", "[[]").replaceAll("*", "[*]").replaceAll("?", "[?]");
@@ -1,76 +1,237 @@
1
+ import { PGlite } from "@electric-sql/pglite";
2
+ import {
3
+ boolean,
4
+ doublePrecision,
5
+ integer,
6
+ jsonb,
7
+ pgTable,
8
+ text,
9
+ varchar,
10
+ } from "drizzle-orm/pg-core";
11
+ import { drizzle } from "drizzle-orm/pglite";
1
12
  import { describe, expect, it, vi } from "vitest";
2
13
 
3
- import { generateSchema } from "../db";
4
- import { createHotUpdater } from "../index";
14
+ import { setupDatabasePluginTestSuite } from "../../../test-utils/src/setupDatabasePluginTestSuite";
15
+ import type { DatabaseAdapterWithCapabilities } from "../db/types";
16
+ import {
17
+ DATABASE_PLUGIN_TEST_RESET_SQL,
18
+ DATABASE_PLUGIN_TEST_SCHEMA_SQL,
19
+ } from "./databasePluginTestDatabase";
5
20
  import { drizzleAdapter } from "./drizzle";
6
21
 
7
- describe("drizzleAdapter", () => {
8
- it("generates schema without resolving a lazy runtime database", () => {
22
+ const bundles = pgTable("bundles", {
23
+ id: text("id").primaryKey(),
24
+ platform: text("platform").notNull(),
25
+ file_hash: text("file_hash").notNull(),
26
+ git_commit_hash: text("git_commit_hash"),
27
+ storage_uri: text("storage_uri").notNull(),
28
+ archive_byte_size: doublePrecision("archive_byte_size").notNull(),
29
+ metadata: jsonb("metadata").notNull(),
30
+ manifest_storage_uri: text("manifest_storage_uri"),
31
+ manifest_file_hash: text("manifest_file_hash"),
32
+ asset_base_storage_uri: text("asset_base_storage_uri"),
33
+ });
34
+ const channels = pgTable("channels", {
35
+ id: text("id").primaryKey(),
36
+ name: text("name").notNull().unique(),
37
+ });
38
+ const releases = pgTable("releases", {
39
+ id: text("id").primaryKey(),
40
+ revision: integer("revision").notNull(),
41
+ scope_key: text("scope_key").notNull(),
42
+ channel_id: text("channel_id").notNull(),
43
+ platform: text("platform").notNull(),
44
+ kind: text("kind").notNull(),
45
+ bundle_id: text("bundle_id"),
46
+ strategy: text("strategy").notNull(),
47
+ target_app_version: text("target_app_version"),
48
+ fingerprint_hash: text("fingerprint_hash"),
49
+ enabled: boolean("enabled").notNull(),
50
+ should_force_update: boolean("should_force_update").notNull(),
51
+ message: text("message"),
52
+ rollout_cohort_count: integer("rollout_cohort_count").notNull(),
53
+ target_cohorts: jsonb("target_cohorts").notNull(),
54
+ operation: text("operation").notNull(),
55
+ source_release_id: text("source_release_id"),
56
+ created_at_ms: integer("created_at_ms").notNull(),
57
+ updated_at_ms: integer("updated_at_ms").notNull(),
58
+ });
59
+ const releaseCatalogs = pgTable("release_catalogs", {
60
+ scope_key: text("scope_key").primaryKey(),
61
+ catalog_id: text("catalog_id").notNull(),
62
+ strategy: text("strategy").notNull(),
63
+ channel_id: text("channel_id").notNull(),
64
+ channel_key: text("channel_key").notNull(),
65
+ platform: text("platform").notNull(),
66
+ fingerprint_hash: text("fingerprint_hash"),
67
+ generation: integer("generation").notNull(),
68
+ payload: text("payload").notNull(),
69
+ catalog_hash: text("catalog_hash").notNull(),
70
+ byte_size: integer("byte_size").notNull(),
71
+ is_tombstone: boolean("is_tombstone").notNull(),
72
+ updated_at_ms: integer("updated_at_ms").notNull(),
73
+ });
74
+ const bundlePatches = pgTable("bundle_patches", {
75
+ id: varchar("id", { length: 255 }).primaryKey(),
76
+ bundle_id: text("bundle_id").notNull(),
77
+ base_bundle_id: text("base_bundle_id").notNull(),
78
+ base_file_hash: text("base_file_hash").notNull(),
79
+ patch_file_hash: text("patch_file_hash").notNull(),
80
+ patch_storage_uri: text("patch_storage_uri").notNull(),
81
+ byte_size: doublePrecision("byte_size").notNull(),
82
+ order_index: integer("order_index").notNull(),
83
+ });
84
+ const bundleEvents = pgTable("bundle_events", {
85
+ id: text("id").primaryKey(),
86
+ type: text("type").notNull(),
87
+ install_id: text("install_id").notNull(),
88
+ user_id: text("user_id"),
89
+ username: text("username"),
90
+ from_release_id: text("from_release_id"),
91
+ from_bundle_id: text("from_bundle_id"),
92
+ to_release_id: text("to_release_id"),
93
+ to_bundle_id: text("to_bundle_id").notNull(),
94
+ platform: text("platform").notNull(),
95
+ app_version: text("app_version").notNull(),
96
+ channel: text("channel").notNull(),
97
+ cohort: text("cohort").notNull(),
98
+ update_strategy: text("update_strategy"),
99
+ fingerprint_hash: text("fingerprint_hash"),
100
+ sdk_version: text("sdk_version"),
101
+ received_at_ms: integer("received_at_ms").notNull(),
102
+ });
103
+ const apiKeys = pgTable("api_keys", {
104
+ id: text("id").primaryKey(),
105
+ hash: text("hash").notNull().unique(),
106
+ name: text("name").notNull(),
107
+ prefix: text("prefix").notNull(),
108
+ role: text("role").notNull(),
109
+ created_at_ms: integer("created_at_ms").notNull(),
110
+ revoked_at_ms: integer("revoked_at_ms"),
111
+ });
112
+ const schema = {
113
+ bundle_events: bundleEvents,
114
+ bundle_patches: bundlePatches,
115
+ bundles,
116
+ channels,
117
+ api_keys: apiKeys,
118
+ release_catalogs: releaseCatalogs,
119
+ releases,
120
+ };
121
+
122
+ class DrizzleTestStateError extends Error {
123
+ readonly name = "DrizzleTestStateError";
124
+ }
125
+
126
+ let client: PGlite | undefined;
127
+ let database: ReturnType<typeof drizzle<typeof schema>> | undefined;
128
+
129
+ const getClient = (): PGlite => {
130
+ if (client === undefined) throw new DrizzleTestStateError();
131
+ return client;
132
+ };
133
+
134
+ const getDatabase = (): ReturnType<typeof drizzle<typeof schema>> => {
135
+ if (database === undefined) throw new DrizzleTestStateError();
136
+ return database;
137
+ };
138
+
139
+ setupDatabasePluginTestSuite({
140
+ name: "drizzleAdapter PostgreSQL",
141
+ migrate: async () => {
142
+ client = new PGlite();
143
+ await client.exec(DATABASE_PLUGIN_TEST_SCHEMA_SQL);
144
+ database = drizzle(client, { schema });
145
+ },
146
+ createPlugin: (): DatabaseAdapterWithCapabilities =>
147
+ drizzleAdapter({ db: getDatabase(), provider: "postgresql" }),
148
+ reset: async () => {
149
+ await getClient().exec(DATABASE_PLUGIN_TEST_RESET_SQL);
150
+ },
151
+ dispose: async () => {
152
+ await getClient().close();
153
+ database = undefined;
154
+ client = undefined;
155
+ },
156
+ });
157
+
158
+ describe("drizzleAdapter schema requirements", () => {
159
+ it("does not resolve a lazy database while generating a schema", () => {
9
160
  const getDB = vi.fn(() => {
10
- throw new Error("runtime database should not be opened");
161
+ throw new DrizzleTestStateError();
11
162
  });
12
-
13
- const hotUpdater = createHotUpdater({
14
- database: drizzleAdapter({
15
- db: getDB,
16
- provider: "postgresql",
17
- }),
163
+ const plugin = drizzleAdapter({
164
+ db: getDB,
165
+ provider: "postgresql",
166
+ schema,
18
167
  });
19
168
 
20
- const schema = generateSchema(hotUpdater, "latest");
169
+ const generated = plugin.generateSchema?.("latest");
21
170
 
22
- expect(schema.path).toBe("hot-updater-schema.ts");
23
- expect(schema.code).toContain("pgTable");
171
+ expect(generated?.code).toContain("pgTable");
24
172
  expect(getDB).not.toHaveBeenCalled();
25
173
  });
26
174
 
27
- it("resolves a lazy runtime database only when a database operation runs", async () => {
28
- const bundles = { channel: "channel" };
29
- const bundlePatches = { bundle_id: "bundle_id" };
30
- const db = {
31
- _: { fullSchema: { bundle_patches: bundlePatches, bundles } },
32
- $count: vi.fn(),
33
- delete: vi.fn(),
34
- insert: vi.fn(),
35
- query: {
36
- bundle_patches: {
37
- findMany: vi.fn(),
38
- },
39
- bundles: {
40
- findFirst: vi.fn(),
41
- findMany: vi.fn(async () => [{ channel: "production" }]),
42
- },
43
- },
44
- select: vi.fn(),
45
- update: vi.fn(),
46
- };
47
- const getDB = vi.fn(async () => db);
48
-
175
+ it("resolves a lazy database on the first database operation", async () => {
176
+ const lazyClient = new PGlite();
177
+ await lazyClient.exec(DATABASE_PLUGIN_TEST_SCHEMA_SQL);
178
+ const lazyDatabase = drizzle(lazyClient, { schema });
179
+ const getDB = vi.fn(async () => lazyDatabase);
49
180
  const plugin = drizzleAdapter({
50
181
  db: getDB,
51
182
  provider: "postgresql",
52
- schema: { bundle_patches: bundlePatches, bundles },
53
- })();
54
-
55
- expect(getDB).not.toHaveBeenCalled();
183
+ schema,
184
+ });
56
185
 
57
- await expect(plugin.getChannels()).resolves.toEqual(["production"]);
58
- expect(getDB).toHaveBeenCalledOnce();
186
+ try {
187
+ expect(getDB).not.toHaveBeenCalled();
188
+ await expect(plugin.models.channels.list({})).resolves.toEqual({
189
+ channels: [],
190
+ });
191
+ expect(getDB).toHaveBeenCalledOnce();
192
+ } finally {
193
+ await lazyClient.close();
194
+ }
59
195
  });
60
196
 
61
- it("requires schema for lazy runtime database configs", async () => {
197
+ it("rejects a lazy database without a schema on first use", () => {
62
198
  const getDB = vi.fn(() => {
63
- throw new Error("runtime database should not be opened");
199
+ throw new DrizzleTestStateError();
64
200
  });
201
+ const plugin = drizzleAdapter({ db: getDB, provider: "postgresql" });
202
+
203
+ expect(() => plugin.models.channels.list({})).toThrow(
204
+ "[hot-updater] Drizzle adapter requires schema when db is lazy.",
205
+ );
206
+ expect(getDB).not.toHaveBeenCalled();
207
+ });
65
208
 
209
+ it("rejects an invalid lazy database schema on first use", () => {
210
+ const getDB = vi.fn(() => {
211
+ throw new DrizzleTestStateError();
212
+ });
66
213
  const plugin = drizzleAdapter({
67
214
  db: getDB,
68
215
  provider: "postgresql",
69
- })();
216
+ schema: { ...schema, bundles: null },
217
+ });
70
218
 
71
- await expect(plugin.getChannels()).rejects.toThrow(
72
- "[hot-updater] Drizzle adapter requires schema when db is lazy.",
219
+ expect(() => plugin.models.channels.list({})).toThrow(
220
+ '[hot-updater] Drizzle schema table "bundles" is invalid.',
73
221
  );
74
222
  expect(getDB).not.toHaveBeenCalled();
75
223
  });
224
+
225
+ it("requires all fixed table objects on first use", () => {
226
+ const incompleteSchema = { bundles };
227
+ const plugin = drizzleAdapter({
228
+ db: () => getDatabase(),
229
+ provider: "postgresql",
230
+ schema: incompleteSchema,
231
+ });
232
+
233
+ expect(() => plugin.models.channels.list({})).toThrow(
234
+ 'Drizzle schema is missing table "bundle_patches".',
235
+ );
236
+ });
76
237
  });