@hot-updater/server 0.21.9 → 0.21.11
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.
- package/dist/db/index.cjs +16 -271
- package/dist/db/index.d.cts +3 -48
- package/dist/db/index.d.ts +3 -48
- package/dist/db/index.js +17 -267
- package/dist/db/ormCore.cjs +278 -0
- package/dist/db/ormCore.d.cts +26 -0
- package/dist/db/ormCore.d.ts +26 -0
- package/dist/db/ormCore.js +273 -0
- package/dist/db/pluginCore.cjs +71 -0
- package/dist/db/pluginCore.js +69 -0
- package/dist/db/types.cjs +12 -0
- package/dist/db/types.d.cts +31 -0
- package/dist/db/types.d.ts +31 -0
- package/dist/db/types.js +10 -0
- package/dist/index.cjs +2 -1
- package/dist/index.d.cts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/package.json +10 -9
- package/src/db/index.spec.ts +99 -1
- package/src/db/index.ts +38 -446
- package/src/db/ormCore.ts +441 -0
- package/src/db/pluginCore.ts +119 -0
- package/src/db/types.ts +57 -0
- package/src/handler-standalone-integration.spec.ts +9 -9
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AppUpdateInfo,
|
|
3
|
+
AppVersionGetBundlesArgs,
|
|
4
|
+
Bundle,
|
|
5
|
+
FingerprintGetBundlesArgs,
|
|
6
|
+
GetBundlesArgs,
|
|
7
|
+
Platform,
|
|
8
|
+
UpdateInfo,
|
|
9
|
+
} from "@hot-updater/core";
|
|
10
|
+
import { NIL_UUID } from "@hot-updater/core";
|
|
11
|
+
import { filterCompatibleAppVersions } from "@hot-updater/plugin-core";
|
|
12
|
+
import type { InferFumaDB } from "fumadb";
|
|
13
|
+
import { fumadb } from "fumadb";
|
|
14
|
+
import type { FumaDBAdapter } from "fumadb/adapters";
|
|
15
|
+
import { calculatePagination } from "../calculatePagination";
|
|
16
|
+
import { v0_21_0 } from "../schema/v0_21_0";
|
|
17
|
+
import type { PaginationInfo } from "../types";
|
|
18
|
+
import type { DatabaseAPI } from "./types";
|
|
19
|
+
|
|
20
|
+
export const HotUpdaterDB = fumadb({
|
|
21
|
+
namespace: "hot_updater",
|
|
22
|
+
schemas: [v0_21_0],
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export type HotUpdaterClient = InferFumaDB<typeof HotUpdaterDB>;
|
|
26
|
+
|
|
27
|
+
export type Migrator = ReturnType<HotUpdaterClient["createMigrator"]>;
|
|
28
|
+
|
|
29
|
+
export function createOrmDatabaseCore({
|
|
30
|
+
database,
|
|
31
|
+
resolveFileUrl,
|
|
32
|
+
}: {
|
|
33
|
+
database: FumaDBAdapter;
|
|
34
|
+
resolveFileUrl: (storageUri: string | null) => Promise<string | null>;
|
|
35
|
+
}): {
|
|
36
|
+
api: DatabaseAPI;
|
|
37
|
+
adapterName: string;
|
|
38
|
+
createMigrator: () => Migrator;
|
|
39
|
+
generateSchema: HotUpdaterClient["generateSchema"];
|
|
40
|
+
} {
|
|
41
|
+
const client = HotUpdaterDB.client(database);
|
|
42
|
+
|
|
43
|
+
const api: DatabaseAPI = {
|
|
44
|
+
async getBundleById(id: string): Promise<Bundle | null> {
|
|
45
|
+
const version = await client.version();
|
|
46
|
+
const orm = client.orm(version);
|
|
47
|
+
const result = await orm.findFirst("bundles", {
|
|
48
|
+
select: [
|
|
49
|
+
"id",
|
|
50
|
+
"platform",
|
|
51
|
+
"should_force_update",
|
|
52
|
+
"enabled",
|
|
53
|
+
"file_hash",
|
|
54
|
+
"git_commit_hash",
|
|
55
|
+
"message",
|
|
56
|
+
"channel",
|
|
57
|
+
"storage_uri",
|
|
58
|
+
"target_app_version",
|
|
59
|
+
"fingerprint_hash",
|
|
60
|
+
"metadata",
|
|
61
|
+
],
|
|
62
|
+
where: (b) => b("id", "=", id),
|
|
63
|
+
});
|
|
64
|
+
if (!result) return null;
|
|
65
|
+
const bundle: Bundle = {
|
|
66
|
+
id: result.id,
|
|
67
|
+
platform: result.platform as Platform,
|
|
68
|
+
shouldForceUpdate: Boolean(result.should_force_update),
|
|
69
|
+
enabled: Boolean(result.enabled),
|
|
70
|
+
fileHash: result.file_hash,
|
|
71
|
+
gitCommitHash: result.git_commit_hash ?? null,
|
|
72
|
+
message: result.message ?? null,
|
|
73
|
+
channel: result.channel,
|
|
74
|
+
storageUri: result.storage_uri,
|
|
75
|
+
targetAppVersion: result.target_app_version ?? null,
|
|
76
|
+
fingerprintHash: result.fingerprint_hash ?? null,
|
|
77
|
+
};
|
|
78
|
+
return bundle;
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
async getUpdateInfo(args: GetBundlesArgs): Promise<UpdateInfo | null> {
|
|
82
|
+
const version = await client.version();
|
|
83
|
+
const orm = client.orm(version);
|
|
84
|
+
|
|
85
|
+
type UpdateSelectRow = {
|
|
86
|
+
id: string;
|
|
87
|
+
should_force_update: boolean;
|
|
88
|
+
message: string | null;
|
|
89
|
+
storage_uri: string | null;
|
|
90
|
+
file_hash: string;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const toUpdateInfo = (
|
|
94
|
+
row: UpdateSelectRow,
|
|
95
|
+
status: "UPDATE" | "ROLLBACK",
|
|
96
|
+
): UpdateInfo => ({
|
|
97
|
+
id: row.id,
|
|
98
|
+
shouldForceUpdate:
|
|
99
|
+
status === "ROLLBACK" ? true : Boolean(row.should_force_update),
|
|
100
|
+
message: row.message ?? null,
|
|
101
|
+
status,
|
|
102
|
+
storageUri: row.storage_uri ?? null,
|
|
103
|
+
fileHash: row.file_hash ?? null,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const INIT_BUNDLE_ROLLBACK_UPDATE_INFO: UpdateInfo = {
|
|
107
|
+
id: NIL_UUID,
|
|
108
|
+
message: null,
|
|
109
|
+
shouldForceUpdate: true,
|
|
110
|
+
status: "ROLLBACK",
|
|
111
|
+
storageUri: null,
|
|
112
|
+
fileHash: null,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const appVersionStrategy = async ({
|
|
116
|
+
platform,
|
|
117
|
+
appVersion,
|
|
118
|
+
bundleId,
|
|
119
|
+
minBundleId = NIL_UUID,
|
|
120
|
+
channel = "production",
|
|
121
|
+
}: AppVersionGetBundlesArgs): Promise<UpdateInfo | null> => {
|
|
122
|
+
const versionRows = await orm.findMany("bundles", {
|
|
123
|
+
select: ["target_app_version"],
|
|
124
|
+
where: (b) => b.and(b("platform", "=", platform)),
|
|
125
|
+
});
|
|
126
|
+
const allTargetVersions = Array.from(
|
|
127
|
+
new Set(
|
|
128
|
+
(versionRows ?? [])
|
|
129
|
+
.map((r) => r.target_app_version)
|
|
130
|
+
.filter((v): v is string => Boolean(v)),
|
|
131
|
+
),
|
|
132
|
+
);
|
|
133
|
+
const compatibleVersions = filterCompatibleAppVersions(
|
|
134
|
+
allTargetVersions,
|
|
135
|
+
appVersion,
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const baseRows =
|
|
139
|
+
compatibleVersions.length === 0
|
|
140
|
+
? []
|
|
141
|
+
: await orm.findMany("bundles", {
|
|
142
|
+
select: [
|
|
143
|
+
"id",
|
|
144
|
+
"should_force_update",
|
|
145
|
+
"message",
|
|
146
|
+
"storage_uri",
|
|
147
|
+
"file_hash",
|
|
148
|
+
"channel",
|
|
149
|
+
"target_app_version",
|
|
150
|
+
"enabled",
|
|
151
|
+
],
|
|
152
|
+
where: (b) =>
|
|
153
|
+
b.and(
|
|
154
|
+
b("enabled", "=", true),
|
|
155
|
+
b("platform", "=", platform),
|
|
156
|
+
b("id", ">=", minBundleId ?? NIL_UUID),
|
|
157
|
+
b("channel", "=", channel),
|
|
158
|
+
b.isNotNull("target_app_version"),
|
|
159
|
+
),
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const candidates = (baseRows ?? []).filter((r) =>
|
|
163
|
+
r.target_app_version
|
|
164
|
+
? compatibleVersions.includes(r.target_app_version)
|
|
165
|
+
: false,
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const byIdDesc = (a: { id: string }, b: { id: string }) =>
|
|
169
|
+
b.id.localeCompare(a.id);
|
|
170
|
+
const sorted = (candidates ?? []).slice().sort(byIdDesc);
|
|
171
|
+
|
|
172
|
+
const latestCandidate = sorted[0] ?? null;
|
|
173
|
+
const currentBundle = sorted.find((b) => b.id === bundleId);
|
|
174
|
+
const updateCandidate =
|
|
175
|
+
sorted.find((b) => b.id.localeCompare(bundleId) > 0) ?? null;
|
|
176
|
+
const rollbackCandidate =
|
|
177
|
+
sorted.find((b) => b.id.localeCompare(bundleId) < 0) ?? null;
|
|
178
|
+
|
|
179
|
+
if (bundleId === NIL_UUID) {
|
|
180
|
+
if (latestCandidate && latestCandidate.id !== bundleId) {
|
|
181
|
+
return toUpdateInfo(latestCandidate, "UPDATE");
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (currentBundle) {
|
|
187
|
+
if (
|
|
188
|
+
latestCandidate &&
|
|
189
|
+
latestCandidate.id.localeCompare(currentBundle.id) > 0
|
|
190
|
+
) {
|
|
191
|
+
return toUpdateInfo(latestCandidate, "UPDATE");
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (updateCandidate) {
|
|
197
|
+
return toUpdateInfo(updateCandidate, "UPDATE");
|
|
198
|
+
}
|
|
199
|
+
if (rollbackCandidate) {
|
|
200
|
+
return toUpdateInfo(rollbackCandidate, "ROLLBACK");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (minBundleId && bundleId.localeCompare(minBundleId) <= 0) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
return INIT_BUNDLE_ROLLBACK_UPDATE_INFO;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const fingerprintStrategy = async ({
|
|
210
|
+
platform,
|
|
211
|
+
fingerprintHash,
|
|
212
|
+
bundleId,
|
|
213
|
+
minBundleId = NIL_UUID,
|
|
214
|
+
channel = "production",
|
|
215
|
+
}: FingerprintGetBundlesArgs): Promise<UpdateInfo | null> => {
|
|
216
|
+
const candidates = await orm.findMany("bundles", {
|
|
217
|
+
select: [
|
|
218
|
+
"id",
|
|
219
|
+
"should_force_update",
|
|
220
|
+
"message",
|
|
221
|
+
"storage_uri",
|
|
222
|
+
"file_hash",
|
|
223
|
+
"channel",
|
|
224
|
+
"fingerprint_hash",
|
|
225
|
+
"enabled",
|
|
226
|
+
],
|
|
227
|
+
where: (b) =>
|
|
228
|
+
b.and(
|
|
229
|
+
b("enabled", "=", true),
|
|
230
|
+
b("platform", "=", platform),
|
|
231
|
+
b("id", ">=", minBundleId ?? NIL_UUID),
|
|
232
|
+
b("channel", "=", channel),
|
|
233
|
+
b("fingerprint_hash", "=", fingerprintHash),
|
|
234
|
+
),
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const byIdDesc = (a: { id: string }, b: { id: string }) =>
|
|
238
|
+
b.id.localeCompare(a.id);
|
|
239
|
+
const sorted = (candidates ?? []).slice().sort(byIdDesc);
|
|
240
|
+
|
|
241
|
+
const latestCandidate = sorted[0] ?? null;
|
|
242
|
+
const currentBundle = sorted.find((b) => b.id === bundleId);
|
|
243
|
+
const updateCandidate =
|
|
244
|
+
sorted.find((b) => b.id.localeCompare(bundleId) > 0) ?? null;
|
|
245
|
+
const rollbackCandidate =
|
|
246
|
+
sorted.find((b) => b.id.localeCompare(bundleId) < 0) ?? null;
|
|
247
|
+
|
|
248
|
+
if (bundleId === NIL_UUID) {
|
|
249
|
+
if (latestCandidate && latestCandidate.id !== bundleId) {
|
|
250
|
+
return toUpdateInfo(latestCandidate, "UPDATE");
|
|
251
|
+
}
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (currentBundle) {
|
|
256
|
+
if (
|
|
257
|
+
latestCandidate &&
|
|
258
|
+
latestCandidate.id.localeCompare(currentBundle.id) > 0
|
|
259
|
+
) {
|
|
260
|
+
return toUpdateInfo(latestCandidate, "UPDATE");
|
|
261
|
+
}
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (updateCandidate) {
|
|
266
|
+
return toUpdateInfo(updateCandidate, "UPDATE");
|
|
267
|
+
}
|
|
268
|
+
if (rollbackCandidate) {
|
|
269
|
+
return toUpdateInfo(rollbackCandidate, "ROLLBACK");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (minBundleId && bundleId.localeCompare(minBundleId) <= 0) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
return INIT_BUNDLE_ROLLBACK_UPDATE_INFO;
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
if (args._updateStrategy === "appVersion") {
|
|
279
|
+
return appVersionStrategy(args);
|
|
280
|
+
}
|
|
281
|
+
if (args._updateStrategy === "fingerprint") {
|
|
282
|
+
return fingerprintStrategy(args);
|
|
283
|
+
}
|
|
284
|
+
return null;
|
|
285
|
+
},
|
|
286
|
+
|
|
287
|
+
async getAppUpdateInfo(
|
|
288
|
+
args: GetBundlesArgs,
|
|
289
|
+
): Promise<AppUpdateInfo | null> {
|
|
290
|
+
const info = await this.getUpdateInfo(args);
|
|
291
|
+
if (!info) return null;
|
|
292
|
+
const { storageUri, ...rest } = info as UpdateInfo & {
|
|
293
|
+
storageUri: string | null;
|
|
294
|
+
};
|
|
295
|
+
const fileUrl = await resolveFileUrl(storageUri ?? null);
|
|
296
|
+
return { ...rest, fileUrl };
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
async getChannels(): Promise<string[]> {
|
|
300
|
+
const version = await client.version();
|
|
301
|
+
const orm = client.orm(version);
|
|
302
|
+
const rows = await orm.findMany("bundles", {
|
|
303
|
+
select: ["channel"],
|
|
304
|
+
});
|
|
305
|
+
const set = new Set(rows?.map((r) => r.channel) ?? []);
|
|
306
|
+
return Array.from(set);
|
|
307
|
+
},
|
|
308
|
+
|
|
309
|
+
async getBundles(options: {
|
|
310
|
+
where?: { channel?: string; platform?: string };
|
|
311
|
+
limit: number;
|
|
312
|
+
offset: number;
|
|
313
|
+
}): Promise<{ data: Bundle[]; pagination: PaginationInfo }> {
|
|
314
|
+
const version = await client.version();
|
|
315
|
+
const orm = client.orm(version);
|
|
316
|
+
const { where, limit, offset } = options;
|
|
317
|
+
|
|
318
|
+
const rows = await orm.findMany("bundles", {
|
|
319
|
+
select: [
|
|
320
|
+
"id",
|
|
321
|
+
"platform",
|
|
322
|
+
"should_force_update",
|
|
323
|
+
"enabled",
|
|
324
|
+
"file_hash",
|
|
325
|
+
"git_commit_hash",
|
|
326
|
+
"message",
|
|
327
|
+
"channel",
|
|
328
|
+
"storage_uri",
|
|
329
|
+
"target_app_version",
|
|
330
|
+
"fingerprint_hash",
|
|
331
|
+
"metadata",
|
|
332
|
+
],
|
|
333
|
+
where: (b) => {
|
|
334
|
+
const conditions = [];
|
|
335
|
+
if (where?.channel) {
|
|
336
|
+
conditions.push(b("channel", "=", where.channel));
|
|
337
|
+
}
|
|
338
|
+
if (where?.platform) {
|
|
339
|
+
conditions.push(b("platform", "=", where.platform));
|
|
340
|
+
}
|
|
341
|
+
return conditions.length > 0 ? b.and(...conditions) : true;
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
const all: Bundle[] = rows
|
|
346
|
+
.map(
|
|
347
|
+
(r): Bundle => ({
|
|
348
|
+
id: r.id,
|
|
349
|
+
platform: r.platform as Platform,
|
|
350
|
+
shouldForceUpdate: Boolean(r.should_force_update),
|
|
351
|
+
enabled: Boolean(r.enabled),
|
|
352
|
+
fileHash: r.file_hash,
|
|
353
|
+
gitCommitHash: r.git_commit_hash ?? null,
|
|
354
|
+
message: r.message ?? null,
|
|
355
|
+
channel: r.channel,
|
|
356
|
+
storageUri: r.storage_uri,
|
|
357
|
+
targetAppVersion: r.target_app_version ?? null,
|
|
358
|
+
fingerprintHash: r.fingerprint_hash ?? null,
|
|
359
|
+
}),
|
|
360
|
+
)
|
|
361
|
+
.sort((a, b) => b.id.localeCompare(a.id));
|
|
362
|
+
|
|
363
|
+
const total = all.length;
|
|
364
|
+
const sliced = all.slice(offset, offset + limit);
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
data: sliced,
|
|
368
|
+
pagination: calculatePagination(total, { limit, offset }),
|
|
369
|
+
};
|
|
370
|
+
},
|
|
371
|
+
|
|
372
|
+
async insertBundle(bundle: Bundle): Promise<void> {
|
|
373
|
+
const version = await client.version();
|
|
374
|
+
const orm = client.orm(version);
|
|
375
|
+
const values = {
|
|
376
|
+
id: bundle.id,
|
|
377
|
+
platform: bundle.platform,
|
|
378
|
+
should_force_update: bundle.shouldForceUpdate,
|
|
379
|
+
enabled: bundle.enabled,
|
|
380
|
+
file_hash: bundle.fileHash,
|
|
381
|
+
git_commit_hash: bundle.gitCommitHash,
|
|
382
|
+
message: bundle.message,
|
|
383
|
+
channel: bundle.channel,
|
|
384
|
+
storage_uri: bundle.storageUri,
|
|
385
|
+
target_app_version: bundle.targetAppVersion,
|
|
386
|
+
fingerprint_hash: bundle.fingerprintHash,
|
|
387
|
+
metadata: bundle.metadata ?? {},
|
|
388
|
+
};
|
|
389
|
+
const { id, ...updateValues } = values;
|
|
390
|
+
await orm.upsert("bundles", {
|
|
391
|
+
where: (b) => b("id", "=", id),
|
|
392
|
+
create: values,
|
|
393
|
+
update: updateValues,
|
|
394
|
+
});
|
|
395
|
+
},
|
|
396
|
+
|
|
397
|
+
async updateBundleById(
|
|
398
|
+
bundleId: string,
|
|
399
|
+
newBundle: Partial<Bundle>,
|
|
400
|
+
): Promise<void> {
|
|
401
|
+
const version = await client.version();
|
|
402
|
+
const orm = client.orm(version);
|
|
403
|
+
const current = await this.getBundleById(bundleId);
|
|
404
|
+
if (!current) throw new Error("targetBundleId not found");
|
|
405
|
+
const merged: Bundle = { ...current, ...newBundle };
|
|
406
|
+
const values = {
|
|
407
|
+
id: merged.id,
|
|
408
|
+
platform: merged.platform,
|
|
409
|
+
should_force_update: merged.shouldForceUpdate,
|
|
410
|
+
enabled: merged.enabled,
|
|
411
|
+
file_hash: merged.fileHash,
|
|
412
|
+
git_commit_hash: merged.gitCommitHash,
|
|
413
|
+
message: merged.message,
|
|
414
|
+
channel: merged.channel,
|
|
415
|
+
storage_uri: merged.storageUri,
|
|
416
|
+
target_app_version: merged.targetAppVersion,
|
|
417
|
+
fingerprint_hash: merged.fingerprintHash,
|
|
418
|
+
metadata: merged.metadata ?? {},
|
|
419
|
+
};
|
|
420
|
+
const { id: id2, ...updateValues2 } = values;
|
|
421
|
+
await orm.upsert("bundles", {
|
|
422
|
+
where: (b) => b("id", "=", id2),
|
|
423
|
+
create: values,
|
|
424
|
+
update: updateValues2,
|
|
425
|
+
});
|
|
426
|
+
},
|
|
427
|
+
|
|
428
|
+
async deleteBundleById(bundleId: string): Promise<void> {
|
|
429
|
+
const version = await client.version();
|
|
430
|
+
const orm = client.orm(version);
|
|
431
|
+
await orm.deleteMany("bundles", { where: (b) => b("id", "=", bundleId) });
|
|
432
|
+
},
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
return {
|
|
436
|
+
api,
|
|
437
|
+
adapterName: client.adapter.name,
|
|
438
|
+
createMigrator: () => client.createMigrator(),
|
|
439
|
+
generateSchema: client.generateSchema,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AppUpdateInfo,
|
|
3
|
+
Bundle,
|
|
4
|
+
GetBundlesArgs,
|
|
5
|
+
UpdateInfo,
|
|
6
|
+
} from "@hot-updater/core";
|
|
7
|
+
import { getUpdateInfo as getUpdateInfoJS } from "@hot-updater/js";
|
|
8
|
+
import type { DatabasePlugin } from "@hot-updater/plugin-core";
|
|
9
|
+
import type { DatabaseAPI } from "./types";
|
|
10
|
+
|
|
11
|
+
export function createPluginDatabaseCore(
|
|
12
|
+
plugin: DatabasePlugin,
|
|
13
|
+
resolveFileUrl: (storageUri: string | null) => Promise<string | null>,
|
|
14
|
+
): {
|
|
15
|
+
api: DatabaseAPI;
|
|
16
|
+
adapterName: string;
|
|
17
|
+
createMigrator: () => never;
|
|
18
|
+
generateSchema: () => never;
|
|
19
|
+
} {
|
|
20
|
+
const api: DatabaseAPI = {
|
|
21
|
+
async getBundleById(id: string): Promise<Bundle | null> {
|
|
22
|
+
return plugin.getBundleById(id);
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
async getUpdateInfo(args: GetBundlesArgs): Promise<UpdateInfo | null> {
|
|
26
|
+
const where: { channel?: string; platform?: string } = {};
|
|
27
|
+
|
|
28
|
+
if ("platform" in args && args.platform) {
|
|
29
|
+
where.platform = args.platform;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const channel =
|
|
33
|
+
"channel" in args && args.channel ? args.channel : "production";
|
|
34
|
+
where.channel = channel;
|
|
35
|
+
|
|
36
|
+
const { pagination } = await plugin.getBundles({
|
|
37
|
+
where,
|
|
38
|
+
limit: 1,
|
|
39
|
+
offset: 0,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (pagination.total === 0) {
|
|
43
|
+
return getUpdateInfoJS([], args);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const { data } = await plugin.getBundles({
|
|
47
|
+
where,
|
|
48
|
+
limit: pagination.total,
|
|
49
|
+
offset: 0,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const bundles = data;
|
|
53
|
+
return getUpdateInfoJS(bundles, args);
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
async getAppUpdateInfo(
|
|
57
|
+
args: GetBundlesArgs,
|
|
58
|
+
): Promise<AppUpdateInfo | null> {
|
|
59
|
+
const info = await this.getUpdateInfo(args);
|
|
60
|
+
if (!info) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const { storageUri, ...rest } = info as UpdateInfo & {
|
|
64
|
+
storageUri: string | null;
|
|
65
|
+
};
|
|
66
|
+
const fileUrl = await resolveFileUrl(storageUri ?? null);
|
|
67
|
+
return { ...rest, fileUrl };
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
async getChannels(): Promise<string[]> {
|
|
71
|
+
return plugin.getChannels();
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
async getBundles(options: {
|
|
75
|
+
where?: { channel?: string; platform?: string };
|
|
76
|
+
limit: number;
|
|
77
|
+
offset: number;
|
|
78
|
+
}) {
|
|
79
|
+
return plugin.getBundles(options);
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
async insertBundle(bundle: Bundle): Promise<void> {
|
|
83
|
+
await plugin.appendBundle(bundle);
|
|
84
|
+
await plugin.commitBundle();
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
async updateBundleById(
|
|
88
|
+
bundleId: string,
|
|
89
|
+
newBundle: Partial<Bundle>,
|
|
90
|
+
): Promise<void> {
|
|
91
|
+
await plugin.updateBundle(bundleId, newBundle);
|
|
92
|
+
await plugin.commitBundle();
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
async deleteBundleById(bundleId: string): Promise<void> {
|
|
96
|
+
const bundle = await plugin.getBundleById(bundleId);
|
|
97
|
+
if (!bundle) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
await plugin.deleteBundle(bundle);
|
|
101
|
+
await plugin.commitBundle();
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
api,
|
|
107
|
+
adapterName: plugin.name,
|
|
108
|
+
createMigrator: () => {
|
|
109
|
+
throw new Error(
|
|
110
|
+
"createMigrator is only available for Kysely/Prisma/Drizzle database adapters.",
|
|
111
|
+
);
|
|
112
|
+
},
|
|
113
|
+
generateSchema: () => {
|
|
114
|
+
throw new Error(
|
|
115
|
+
"generateSchema is only available for Kysely/Prisma/Drizzle database adapters.",
|
|
116
|
+
);
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
package/src/db/types.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AppUpdateInfo,
|
|
3
|
+
Bundle,
|
|
4
|
+
GetBundlesArgs,
|
|
5
|
+
UpdateInfo,
|
|
6
|
+
} from "@hot-updater/core";
|
|
7
|
+
import type { DatabasePlugin, StoragePlugin } from "@hot-updater/plugin-core";
|
|
8
|
+
import type { FumaDBAdapter } from "fumadb/adapters";
|
|
9
|
+
import type { PaginationInfo } from "../types";
|
|
10
|
+
|
|
11
|
+
export type DatabasePluginFactory = () => DatabasePlugin;
|
|
12
|
+
|
|
13
|
+
export type DatabaseAdapter =
|
|
14
|
+
| FumaDBAdapter
|
|
15
|
+
| DatabasePlugin
|
|
16
|
+
| DatabasePluginFactory;
|
|
17
|
+
|
|
18
|
+
export function isDatabasePluginFactory(
|
|
19
|
+
adapter: DatabaseAdapter,
|
|
20
|
+
): adapter is DatabasePluginFactory {
|
|
21
|
+
return typeof adapter === "function";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function isDatabasePlugin(
|
|
25
|
+
adapter: DatabaseAdapter,
|
|
26
|
+
): adapter is DatabasePlugin {
|
|
27
|
+
return (
|
|
28
|
+
typeof adapter === "object" &&
|
|
29
|
+
adapter !== null &&
|
|
30
|
+
"getBundleById" in adapter &&
|
|
31
|
+
"getBundles" in adapter &&
|
|
32
|
+
"getChannels" in adapter
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isFumaAdapter(
|
|
37
|
+
adapter: DatabaseAdapter,
|
|
38
|
+
): adapter is FumaDBAdapter {
|
|
39
|
+
return !isDatabasePluginFactory(adapter) && !isDatabasePlugin(adapter);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface DatabaseAPI {
|
|
43
|
+
getBundleById(id: string): Promise<Bundle | null>;
|
|
44
|
+
getUpdateInfo(args: GetBundlesArgs): Promise<UpdateInfo | null>;
|
|
45
|
+
getAppUpdateInfo(args: GetBundlesArgs): Promise<AppUpdateInfo | null>;
|
|
46
|
+
getChannels(): Promise<string[]>;
|
|
47
|
+
getBundles(options: {
|
|
48
|
+
where?: { channel?: string; platform?: string };
|
|
49
|
+
limit: number;
|
|
50
|
+
offset: number;
|
|
51
|
+
}): Promise<{ data: Bundle[]; pagination: PaginationInfo }>;
|
|
52
|
+
insertBundle(bundle: Bundle): Promise<void>;
|
|
53
|
+
updateBundleById(bundleId: string, newBundle: Partial<Bundle>): Promise<void>;
|
|
54
|
+
deleteBundleById(bundleId: string): Promise<void>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type StoragePluginFactory = () => StoragePlugin;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { PGlite } from "@electric-sql/pglite";
|
|
2
2
|
import type { Bundle } from "@hot-updater/core";
|
|
3
3
|
import { NIL_UUID } from "@hot-updater/core";
|
|
4
|
-
import { kyselyAdapter } from "@hot-updater/server/adapters/kysely";
|
|
5
4
|
import { standaloneRepository } from "@hot-updater/standalone";
|
|
6
5
|
import { Kysely } from "kysely";
|
|
7
6
|
import { PGliteDialect } from "kysely-pglite-dialect";
|
|
@@ -9,6 +8,7 @@ import { HttpResponse, http } from "msw";
|
|
|
9
8
|
import { setupServer } from "msw/node";
|
|
10
9
|
import { uuidv7 } from "uuidv7";
|
|
11
10
|
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
11
|
+
import { kyselyAdapter } from "./adapters/kysely";
|
|
12
12
|
import { createHotUpdater } from "./db";
|
|
13
13
|
|
|
14
14
|
/**
|
|
@@ -108,7 +108,7 @@ describe("Handler <-> Standalone Repository Integration", () => {
|
|
|
108
108
|
// Create standalone repository pointing to our test server
|
|
109
109
|
const repo = standaloneRepository({
|
|
110
110
|
baseUrl: `${baseUrl}/hot-updater`,
|
|
111
|
-
})(
|
|
111
|
+
})();
|
|
112
112
|
|
|
113
113
|
const bundleId = uuidv7();
|
|
114
114
|
const bundle = createTestBundle({
|
|
@@ -146,7 +146,7 @@ describe("Handler <-> Standalone Repository Integration", () => {
|
|
|
146
146
|
// Create standalone repository
|
|
147
147
|
const repo = standaloneRepository({
|
|
148
148
|
baseUrl: `${baseUrl}/hot-updater`,
|
|
149
|
-
})(
|
|
149
|
+
})();
|
|
150
150
|
|
|
151
151
|
// Use standalone repository to retrieve
|
|
152
152
|
const retrieved = await repo.getBundleById(bundleId);
|
|
@@ -173,7 +173,7 @@ describe("Handler <-> Standalone Repository Integration", () => {
|
|
|
173
173
|
// Create standalone repository
|
|
174
174
|
const repo = standaloneRepository({
|
|
175
175
|
baseUrl: `${baseUrl}/hot-updater`,
|
|
176
|
-
})(
|
|
176
|
+
})();
|
|
177
177
|
|
|
178
178
|
// Delete via standalone repository
|
|
179
179
|
await repo.deleteBundle(bundle);
|
|
@@ -199,7 +199,7 @@ describe("Handler <-> Standalone Repository Integration", () => {
|
|
|
199
199
|
// Create standalone repository
|
|
200
200
|
const repo = standaloneRepository({
|
|
201
201
|
baseUrl: `${baseUrl}/hot-updater`,
|
|
202
|
-
})(
|
|
202
|
+
})();
|
|
203
203
|
|
|
204
204
|
// Get all bundles
|
|
205
205
|
const result = await repo.getBundles({ limit: 50, offset: 0 });
|
|
@@ -220,7 +220,7 @@ describe("Handler <-> Standalone Repository Integration", () => {
|
|
|
220
220
|
it("Full E2E: create → retrieve → update → delete via standalone", async () => {
|
|
221
221
|
const repo = standaloneRepository({
|
|
222
222
|
baseUrl: `${baseUrl}/hot-updater`,
|
|
223
|
-
})(
|
|
223
|
+
})();
|
|
224
224
|
|
|
225
225
|
// Step 1: Create bundle via standalone
|
|
226
226
|
const bundleId = uuidv7();
|
|
@@ -258,7 +258,7 @@ describe("Handler <-> Standalone Repository Integration", () => {
|
|
|
258
258
|
it("Multiple bundles in single commit (standalone sends array)", async () => {
|
|
259
259
|
const repo = standaloneRepository({
|
|
260
260
|
baseUrl: `${baseUrl}/hot-updater`,
|
|
261
|
-
})(
|
|
261
|
+
})();
|
|
262
262
|
|
|
263
263
|
// Append multiple bundles
|
|
264
264
|
const bundleId1 = uuidv7();
|
|
@@ -316,7 +316,7 @@ describe("Handler <-> Standalone Repository Integration", () => {
|
|
|
316
316
|
// Create standalone repository with matching basePath
|
|
317
317
|
const repo = standaloneRepository({
|
|
318
318
|
baseUrl: `${baseUrl}/api/v2`,
|
|
319
|
-
})(
|
|
319
|
+
})();
|
|
320
320
|
|
|
321
321
|
// Test create and retrieve
|
|
322
322
|
const bundleId = uuidv7();
|
|
@@ -336,7 +336,7 @@ describe("Handler <-> Standalone Repository Integration", () => {
|
|
|
336
336
|
it("Handler returns 404 when bundle not found (standalone handles gracefully)", async () => {
|
|
337
337
|
const repo = standaloneRepository({
|
|
338
338
|
baseUrl: `${baseUrl}/hot-updater`,
|
|
339
|
-
})(
|
|
339
|
+
})();
|
|
340
340
|
|
|
341
341
|
// Try to get non-existent bundle
|
|
342
342
|
const result = await repo.getBundleById("non-existent-bundle");
|