@hot-updater/server 0.28.0 → 0.29.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.
- package/dist/adapters/drizzle.cjs +7 -7
- package/dist/adapters/drizzle.mjs +2 -0
- package/dist/adapters/kysely.cjs +7 -7
- package/dist/adapters/kysely.mjs +2 -0
- package/dist/adapters/mongodb.cjs +7 -7
- package/dist/adapters/mongodb.mjs +2 -0
- package/dist/adapters/prisma.cjs +7 -7
- package/dist/adapters/prisma.mjs +2 -0
- package/dist/calculatePagination.cjs +1 -3
- package/dist/{calculatePagination.js → calculatePagination.mjs} +1 -2
- package/dist/db/index.cjs +24 -15
- package/dist/db/index.d.cts +12 -9
- package/dist/db/index.d.mts +30 -0
- package/dist/db/index.mjs +45 -0
- package/dist/db/ormCore.cjs +247 -138
- package/dist/db/ormCore.d.cts +35 -17
- package/dist/db/ormCore.d.mts +44 -0
- package/dist/db/ormCore.mjs +386 -0
- package/dist/db/pluginCore.cjs +145 -40
- package/dist/db/pluginCore.mjs +176 -0
- package/dist/db/types.cjs +1 -3
- package/dist/db/types.d.cts +14 -21
- package/dist/db/types.d.mts +24 -0
- package/dist/db/{types.js → types.mjs} +1 -2
- package/dist/handler.cjs +117 -48
- package/dist/handler.d.cts +28 -18
- package/dist/handler.d.mts +47 -0
- package/dist/handler.mjs +217 -0
- package/dist/index.cjs +5 -5
- package/dist/index.d.cts +3 -3
- package/dist/index.d.mts +5 -0
- package/dist/index.mjs +4 -0
- package/dist/internalRouter.cjs +54 -0
- package/dist/internalRouter.mjs +52 -0
- package/dist/node.cjs +2 -3
- package/dist/node.d.cts +0 -1
- package/dist/{node.d.ts → node.d.mts} +1 -2
- package/dist/{node.js → node.mjs} +1 -2
- package/dist/route.cjs +7 -0
- package/dist/route.mjs +7 -0
- package/dist/runtime.cjs +42 -0
- package/dist/runtime.d.cts +21 -0
- package/dist/runtime.d.mts +21 -0
- package/dist/runtime.mjs +40 -0
- package/dist/schema/v0_21_0.cjs +1 -5
- package/dist/schema/{v0_21_0.js → v0_21_0.mjs} +1 -3
- package/dist/schema/v0_29_0.cjs +24 -0
- package/dist/schema/v0_29_0.mjs +24 -0
- package/dist/types/{index.d.ts → index.d.mts} +1 -1
- package/package.json +18 -18
- package/src/db/index.spec.ts +64 -29
- package/src/db/index.ts +55 -35
- package/src/db/ormCore.ts +438 -210
- package/src/db/ormUpdateCheck.bench.ts +261 -0
- package/src/db/pluginCore.ts +298 -49
- package/src/db/pluginUpdateCheck.bench.ts +250 -0
- package/src/db/types.ts +52 -27
- package/src/{handler-standalone-integration.spec.ts → handler-standalone.integration.spec.ts} +106 -0
- package/src/handler.spec.ts +156 -0
- package/src/handler.ts +296 -77
- package/src/internalRouter.ts +104 -0
- package/src/route.ts +7 -0
- package/src/runtime.spec.ts +277 -0
- package/src/runtime.ts +121 -0
- package/src/schema/v0_29_0.ts +26 -0
- package/dist/_virtual/rolldown_runtime.cjs +0 -25
- package/dist/adapters/drizzle.js +0 -3
- package/dist/adapters/kysely.js +0 -3
- package/dist/adapters/mongodb.js +0 -3
- package/dist/adapters/prisma.js +0 -3
- package/dist/db/index.d.ts +0 -27
- package/dist/db/index.js +0 -36
- package/dist/db/ormCore.d.ts +0 -26
- package/dist/db/ormCore.js +0 -273
- package/dist/db/pluginCore.js +0 -69
- package/dist/db/types.d.ts +0 -31
- package/dist/handler.d.ts +0 -37
- package/dist/handler.js +0 -146
- package/dist/index.d.ts +0 -5
- package/dist/index.js +0 -5
- /package/dist/adapters/{drizzle.d.ts → drizzle.d.mts} +0 -0
- /package/dist/adapters/{kysely.d.ts → kysely.d.mts} +0 -0
- /package/dist/adapters/{mongodb.d.ts → mongodb.d.mts} +0 -0
- /package/dist/adapters/{prisma.d.ts → prisma.d.mts} +0 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { NIL_UUID, isCohortEligibleForUpdate } from "@hot-updater/core";
|
|
2
|
+
import { semverSatisfies } from "@hot-updater/plugin-core";
|
|
3
|
+
//#region src/db/pluginCore.ts
|
|
4
|
+
const PAGE_SIZE = 100;
|
|
5
|
+
const DESC_ORDER = {
|
|
6
|
+
field: "id",
|
|
7
|
+
direction: "desc"
|
|
8
|
+
};
|
|
9
|
+
const bundleMatchesQueryWhere = (bundle, where) => {
|
|
10
|
+
if (!where) return true;
|
|
11
|
+
if (where.channel !== void 0 && bundle.channel !== where.channel) return false;
|
|
12
|
+
if (where.platform !== void 0 && bundle.platform !== where.platform) return false;
|
|
13
|
+
if (where.enabled !== void 0 && bundle.enabled !== where.enabled) return false;
|
|
14
|
+
if (where.id?.eq !== void 0 && bundle.id !== where.id.eq) return false;
|
|
15
|
+
if (where.id?.gt !== void 0 && bundle.id.localeCompare(where.id.gt) <= 0) return false;
|
|
16
|
+
if (where.id?.gte !== void 0 && bundle.id.localeCompare(where.id.gte) < 0) return false;
|
|
17
|
+
if (where.id?.lt !== void 0 && bundle.id.localeCompare(where.id.lt) >= 0) return false;
|
|
18
|
+
if (where.id?.lte !== void 0 && bundle.id.localeCompare(where.id.lte) > 0) return false;
|
|
19
|
+
if (where.id?.in && !where.id.in.includes(bundle.id)) return false;
|
|
20
|
+
if (where.targetAppVersionNotNull && bundle.targetAppVersion === null) return false;
|
|
21
|
+
if (where.targetAppVersion !== void 0 && bundle.targetAppVersion !== where.targetAppVersion) return false;
|
|
22
|
+
if (where.targetAppVersionIn && !where.targetAppVersionIn.includes(bundle.targetAppVersion ?? "")) return false;
|
|
23
|
+
if (where.fingerprintHash !== void 0 && bundle.fingerprintHash !== where.fingerprintHash) return false;
|
|
24
|
+
return true;
|
|
25
|
+
};
|
|
26
|
+
const sortBundles = (bundles, orderBy) => {
|
|
27
|
+
const direction = orderBy?.direction ?? "desc";
|
|
28
|
+
return bundles.slice().sort((a, b) => {
|
|
29
|
+
const result = a.id.localeCompare(b.id);
|
|
30
|
+
return direction === "asc" ? result : -result;
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
const makeResponse = (bundle, status) => ({
|
|
34
|
+
id: bundle.id,
|
|
35
|
+
message: bundle.message,
|
|
36
|
+
shouldForceUpdate: status === "ROLLBACK" ? true : bundle.shouldForceUpdate,
|
|
37
|
+
status,
|
|
38
|
+
storageUri: bundle.storageUri,
|
|
39
|
+
fileHash: bundle.fileHash
|
|
40
|
+
});
|
|
41
|
+
const INIT_BUNDLE_ROLLBACK_UPDATE_INFO = {
|
|
42
|
+
message: null,
|
|
43
|
+
id: NIL_UUID,
|
|
44
|
+
shouldForceUpdate: true,
|
|
45
|
+
status: "ROLLBACK",
|
|
46
|
+
storageUri: null,
|
|
47
|
+
fileHash: null
|
|
48
|
+
};
|
|
49
|
+
function createPluginDatabaseCore(plugin, resolveFileUrl) {
|
|
50
|
+
const getSortedBundlePage = async (options, context) => {
|
|
51
|
+
const result = await plugin.getBundles({
|
|
52
|
+
...options,
|
|
53
|
+
orderBy: options.orderBy ?? DESC_ORDER
|
|
54
|
+
}, context);
|
|
55
|
+
return {
|
|
56
|
+
...result,
|
|
57
|
+
data: sortBundles(result.data, options.orderBy ?? DESC_ORDER)
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
const isEligibleForUpdate = (bundle, cohort) => {
|
|
61
|
+
return isCohortEligibleForUpdate(bundle.id, cohort, bundle.rolloutCohortCount, bundle.targetCohorts);
|
|
62
|
+
};
|
|
63
|
+
const findUpdateInfoByScanning = async ({ args, queryWhere, isCandidate, context }) => {
|
|
64
|
+
let offset = 0;
|
|
65
|
+
while (true) {
|
|
66
|
+
const { data, pagination } = await getSortedBundlePage({
|
|
67
|
+
where: queryWhere,
|
|
68
|
+
limit: PAGE_SIZE,
|
|
69
|
+
offset,
|
|
70
|
+
orderBy: DESC_ORDER
|
|
71
|
+
}, context);
|
|
72
|
+
for (const bundle of data) {
|
|
73
|
+
if (!bundleMatchesQueryWhere(bundle, queryWhere) || !isCandidate(bundle)) continue;
|
|
74
|
+
if (args.bundleId === NIL_UUID) {
|
|
75
|
+
if (isEligibleForUpdate(bundle, args.cohort)) return makeResponse(bundle, "UPDATE");
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const compareResult = bundle.id.localeCompare(args.bundleId);
|
|
79
|
+
if (compareResult > 0) {
|
|
80
|
+
if (isEligibleForUpdate(bundle, args.cohort)) return makeResponse(bundle, "UPDATE");
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (compareResult === 0) {
|
|
84
|
+
if (isEligibleForUpdate(bundle, args.cohort)) return null;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
return makeResponse(bundle, "ROLLBACK");
|
|
88
|
+
}
|
|
89
|
+
if (!pagination.hasNextPage) break;
|
|
90
|
+
offset += PAGE_SIZE;
|
|
91
|
+
}
|
|
92
|
+
if (args.bundleId === NIL_UUID) return null;
|
|
93
|
+
if (args.minBundleId && args.bundleId.localeCompare(args.minBundleId) <= 0) return null;
|
|
94
|
+
return INIT_BUNDLE_ROLLBACK_UPDATE_INFO;
|
|
95
|
+
};
|
|
96
|
+
const getBaseWhere = ({ platform, channel, minBundleId }) => ({
|
|
97
|
+
platform,
|
|
98
|
+
channel,
|
|
99
|
+
enabled: true,
|
|
100
|
+
id: { gte: minBundleId }
|
|
101
|
+
});
|
|
102
|
+
return {
|
|
103
|
+
api: {
|
|
104
|
+
async getBundleById(id, context) {
|
|
105
|
+
return plugin.getBundleById(id, context);
|
|
106
|
+
},
|
|
107
|
+
async getUpdateInfo(args, context) {
|
|
108
|
+
const channel = args.channel ?? "production";
|
|
109
|
+
const minBundleId = args.minBundleId ?? NIL_UUID;
|
|
110
|
+
const baseWhere = getBaseWhere({
|
|
111
|
+
platform: args.platform,
|
|
112
|
+
channel,
|
|
113
|
+
minBundleId
|
|
114
|
+
});
|
|
115
|
+
if (args._updateStrategy === "fingerprint") return findUpdateInfoByScanning({
|
|
116
|
+
args,
|
|
117
|
+
queryWhere: {
|
|
118
|
+
...baseWhere,
|
|
119
|
+
fingerprintHash: args.fingerprintHash
|
|
120
|
+
},
|
|
121
|
+
context,
|
|
122
|
+
isCandidate: (bundle) => {
|
|
123
|
+
return bundle.enabled && bundle.platform === args.platform && bundle.channel === channel && bundle.id.localeCompare(minBundleId) >= 0 && bundle.fingerprintHash === args.fingerprintHash;
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
return findUpdateInfoByScanning({
|
|
127
|
+
args,
|
|
128
|
+
queryWhere: { ...baseWhere },
|
|
129
|
+
context,
|
|
130
|
+
isCandidate: (bundle) => {
|
|
131
|
+
return bundle.enabled && bundle.platform === args.platform && bundle.channel === channel && bundle.id.localeCompare(minBundleId) >= 0 && !!bundle.targetAppVersion && semverSatisfies(bundle.targetAppVersion, args.appVersion);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
},
|
|
135
|
+
async getAppUpdateInfo(args, context) {
|
|
136
|
+
const info = await this.getUpdateInfo(args, context);
|
|
137
|
+
if (!info) return null;
|
|
138
|
+
const { storageUri, ...rest } = info;
|
|
139
|
+
const fileUrl = await resolveFileUrl(storageUri ?? null, context);
|
|
140
|
+
return {
|
|
141
|
+
...rest,
|
|
142
|
+
fileUrl
|
|
143
|
+
};
|
|
144
|
+
},
|
|
145
|
+
async getChannels(context) {
|
|
146
|
+
return plugin.getChannels(context);
|
|
147
|
+
},
|
|
148
|
+
async getBundles(options, context) {
|
|
149
|
+
return plugin.getBundles(options, context);
|
|
150
|
+
},
|
|
151
|
+
async insertBundle(bundle, context) {
|
|
152
|
+
await plugin.appendBundle(bundle, context);
|
|
153
|
+
await plugin.commitBundle(context);
|
|
154
|
+
},
|
|
155
|
+
async updateBundleById(bundleId, newBundle, context) {
|
|
156
|
+
await plugin.updateBundle(bundleId, newBundle, context);
|
|
157
|
+
await plugin.commitBundle(context);
|
|
158
|
+
},
|
|
159
|
+
async deleteBundleById(bundleId, context) {
|
|
160
|
+
const bundle = await plugin.getBundleById(bundleId, context);
|
|
161
|
+
if (!bundle) return;
|
|
162
|
+
await plugin.deleteBundle(bundle, context);
|
|
163
|
+
await plugin.commitBundle(context);
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
adapterName: plugin.name,
|
|
167
|
+
createMigrator: () => {
|
|
168
|
+
throw new Error("createMigrator is only available for Kysely/Prisma/Drizzle database adapters.");
|
|
169
|
+
},
|
|
170
|
+
generateSchema: () => {
|
|
171
|
+
throw new Error("generateSchema is only available for Kysely/Prisma/Drizzle database adapters.");
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
//#endregion
|
|
176
|
+
export { createPluginDatabaseCore };
|
package/dist/db/types.cjs
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
//#region src/db/types.ts
|
|
3
2
|
function isDatabasePluginFactory(adapter) {
|
|
4
3
|
return typeof adapter === "function";
|
|
@@ -6,7 +5,6 @@ function isDatabasePluginFactory(adapter) {
|
|
|
6
5
|
function isDatabasePlugin(adapter) {
|
|
7
6
|
return typeof adapter === "object" && adapter !== null && "getBundleById" in adapter && "getBundles" in adapter && "getChannels" in adapter;
|
|
8
7
|
}
|
|
9
|
-
|
|
10
8
|
//#endregion
|
|
11
9
|
exports.isDatabasePlugin = isDatabasePlugin;
|
|
12
|
-
exports.isDatabasePluginFactory = isDatabasePluginFactory;
|
|
10
|
+
exports.isDatabasePluginFactory = isDatabasePluginFactory;
|
package/dist/db/types.d.cts
CHANGED
|
@@ -1,31 +1,24 @@
|
|
|
1
1
|
import { PaginationInfo } from "../types/index.cjs";
|
|
2
|
-
import { DatabasePlugin, StoragePlugin } from "@hot-updater/plugin-core";
|
|
3
|
-
import { FumaDBAdapter } from "fumadb/adapters";
|
|
2
|
+
import { DatabaseBundleQueryOptions, DatabasePlugin, HotUpdaterContext, StoragePlugin } from "@hot-updater/plugin-core";
|
|
4
3
|
import { AppUpdateInfo, Bundle, GetBundlesArgs, UpdateInfo } from "@hot-updater/core";
|
|
4
|
+
import { FumaDBAdapter } from "fumadb/adapters";
|
|
5
5
|
|
|
6
6
|
//#region src/db/types.d.ts
|
|
7
|
-
type DatabasePluginFactory = () => DatabasePlugin
|
|
8
|
-
type DatabaseAdapter = FumaDBAdapter | DatabasePlugin | DatabasePluginFactory
|
|
9
|
-
interface DatabaseAPI {
|
|
10
|
-
getBundleById(id: string): Promise<Bundle | null>;
|
|
11
|
-
getUpdateInfo(args: GetBundlesArgs): Promise<UpdateInfo | null>;
|
|
12
|
-
getAppUpdateInfo(args: GetBundlesArgs): Promise<AppUpdateInfo | null>;
|
|
13
|
-
getChannels(): Promise<string[]>;
|
|
14
|
-
getBundles(options: {
|
|
15
|
-
where?: {
|
|
16
|
-
channel?: string;
|
|
17
|
-
platform?: string;
|
|
18
|
-
};
|
|
19
|
-
limit: number;
|
|
20
|
-
offset: number;
|
|
21
|
-
}): Promise<{
|
|
7
|
+
type DatabasePluginFactory<TContext = unknown> = () => DatabasePlugin<TContext>;
|
|
8
|
+
type DatabaseAdapter<TContext = unknown> = FumaDBAdapter | DatabasePlugin<TContext> | DatabasePluginFactory<TContext>;
|
|
9
|
+
interface DatabaseAPI<TContext = unknown> {
|
|
10
|
+
getBundleById(id: string, context?: HotUpdaterContext<TContext>): Promise<Bundle | null>;
|
|
11
|
+
getUpdateInfo(args: GetBundlesArgs, context?: HotUpdaterContext<TContext>): Promise<UpdateInfo | null>;
|
|
12
|
+
getAppUpdateInfo(args: GetBundlesArgs, context?: HotUpdaterContext<TContext>): Promise<AppUpdateInfo | null>;
|
|
13
|
+
getChannels(context?: HotUpdaterContext<TContext>): Promise<string[]>;
|
|
14
|
+
getBundles(options: DatabaseBundleQueryOptions, context?: HotUpdaterContext<TContext>): Promise<{
|
|
22
15
|
data: Bundle[];
|
|
23
16
|
pagination: PaginationInfo;
|
|
24
17
|
}>;
|
|
25
|
-
insertBundle(bundle: Bundle): Promise<void>;
|
|
26
|
-
updateBundleById(bundleId: string, newBundle: Partial<Bundle>): Promise<void>;
|
|
27
|
-
deleteBundleById(bundleId: string): Promise<void>;
|
|
18
|
+
insertBundle(bundle: Bundle, context?: HotUpdaterContext<TContext>): Promise<void>;
|
|
19
|
+
updateBundleById(bundleId: string, newBundle: Partial<Bundle>, context?: HotUpdaterContext<TContext>): Promise<void>;
|
|
20
|
+
deleteBundleById(bundleId: string, context?: HotUpdaterContext<TContext>): Promise<void>;
|
|
28
21
|
}
|
|
29
|
-
type StoragePluginFactory = () => StoragePlugin
|
|
22
|
+
type StoragePluginFactory<TContext = unknown> = () => StoragePlugin<TContext>;
|
|
30
23
|
//#endregion
|
|
31
24
|
export { DatabaseAPI, DatabaseAdapter, StoragePluginFactory };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { PaginationInfo } from "../types/index.mjs";
|
|
2
|
+
import { AppUpdateInfo, Bundle, GetBundlesArgs, UpdateInfo } from "@hot-updater/core";
|
|
3
|
+
import { DatabaseBundleQueryOptions, DatabasePlugin, HotUpdaterContext, StoragePlugin } from "@hot-updater/plugin-core";
|
|
4
|
+
import { FumaDBAdapter } from "fumadb/adapters";
|
|
5
|
+
|
|
6
|
+
//#region src/db/types.d.ts
|
|
7
|
+
type DatabasePluginFactory<TContext = unknown> = () => DatabasePlugin<TContext>;
|
|
8
|
+
type DatabaseAdapter<TContext = unknown> = FumaDBAdapter | DatabasePlugin<TContext> | DatabasePluginFactory<TContext>;
|
|
9
|
+
interface DatabaseAPI<TContext = unknown> {
|
|
10
|
+
getBundleById(id: string, context?: HotUpdaterContext<TContext>): Promise<Bundle | null>;
|
|
11
|
+
getUpdateInfo(args: GetBundlesArgs, context?: HotUpdaterContext<TContext>): Promise<UpdateInfo | null>;
|
|
12
|
+
getAppUpdateInfo(args: GetBundlesArgs, context?: HotUpdaterContext<TContext>): Promise<AppUpdateInfo | null>;
|
|
13
|
+
getChannels(context?: HotUpdaterContext<TContext>): Promise<string[]>;
|
|
14
|
+
getBundles(options: DatabaseBundleQueryOptions, context?: HotUpdaterContext<TContext>): Promise<{
|
|
15
|
+
data: Bundle[];
|
|
16
|
+
pagination: PaginationInfo;
|
|
17
|
+
}>;
|
|
18
|
+
insertBundle(bundle: Bundle, context?: HotUpdaterContext<TContext>): Promise<void>;
|
|
19
|
+
updateBundleById(bundleId: string, newBundle: Partial<Bundle>, context?: HotUpdaterContext<TContext>): Promise<void>;
|
|
20
|
+
deleteBundleById(bundleId: string, context?: HotUpdaterContext<TContext>): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
type StoragePluginFactory<TContext = unknown> = () => StoragePlugin<TContext>;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { DatabaseAPI, DatabaseAdapter, StoragePluginFactory };
|
|
@@ -5,6 +5,5 @@ function isDatabasePluginFactory(adapter) {
|
|
|
5
5
|
function isDatabasePlugin(adapter) {
|
|
6
6
|
return typeof adapter === "object" && adapter !== null && "getBundleById" in adapter && "getBundles" in adapter && "getChannels" in adapter;
|
|
7
7
|
}
|
|
8
|
-
|
|
9
8
|
//#endregion
|
|
10
|
-
export { isDatabasePlugin, isDatabasePluginFactory };
|
|
9
|
+
export { isDatabasePlugin, isDatabasePluginFactory };
|
package/dist/handler.cjs
CHANGED
|
@@ -1,44 +1,88 @@
|
|
|
1
|
-
const
|
|
2
|
-
let rou3 = require("rou3");
|
|
3
|
-
rou3 = require_rolldown_runtime.__toESM(rou3);
|
|
4
|
-
|
|
1
|
+
const require_internalRouter = require("./internalRouter.cjs");
|
|
5
2
|
//#region src/handler.ts
|
|
3
|
+
var HandlerBadRequestError = class extends Error {
|
|
4
|
+
constructor(message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "HandlerBadRequestError";
|
|
7
|
+
}
|
|
8
|
+
};
|
|
6
9
|
const handleVersion = async () => {
|
|
7
|
-
return new Response(JSON.stringify({ version: "0.
|
|
10
|
+
return new Response(JSON.stringify({ version: "0.29.0" }), {
|
|
8
11
|
status: 200,
|
|
9
12
|
headers: { "Content-Type": "application/json" }
|
|
10
13
|
});
|
|
11
14
|
};
|
|
12
|
-
const
|
|
15
|
+
const decodeMaybe = (value) => {
|
|
16
|
+
if (value === void 0) return void 0;
|
|
17
|
+
try {
|
|
18
|
+
return decodeURIComponent(value);
|
|
19
|
+
} catch {
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const isPlatform = (value) => {
|
|
24
|
+
return value === "ios" || value === "android";
|
|
25
|
+
};
|
|
26
|
+
const requireRouteParam = (params, key) => {
|
|
27
|
+
const value = params[key];
|
|
28
|
+
if (!value) throw new HandlerBadRequestError(`Missing route parameter: ${key}`);
|
|
29
|
+
return value;
|
|
30
|
+
};
|
|
31
|
+
const requirePlatformParam = (params) => {
|
|
32
|
+
const platform = requireRouteParam(params, "platform");
|
|
33
|
+
if (!isPlatform(platform)) throw new HandlerBadRequestError(`Invalid platform: ${platform}. Expected 'ios' or 'android'.`);
|
|
34
|
+
return platform;
|
|
35
|
+
};
|
|
36
|
+
const requireBundlePatchPayload = (payload, bundleId) => {
|
|
37
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) throw new HandlerBadRequestError("Invalid bundle payload");
|
|
38
|
+
const bundlePatch = payload;
|
|
39
|
+
if (bundlePatch.id !== void 0 && bundlePatch.id !== bundleId) throw new HandlerBadRequestError("Bundle id mismatch");
|
|
40
|
+
const { id: _ignoredId, ...rest } = bundlePatch;
|
|
41
|
+
return rest;
|
|
42
|
+
};
|
|
43
|
+
const handleFingerprintUpdateWithCohort = async (params, _request, api, context) => {
|
|
44
|
+
const platform = requirePlatformParam(params);
|
|
45
|
+
const fingerprintHash = requireRouteParam(params, "fingerprintHash");
|
|
46
|
+
const channel = requireRouteParam(params, "channel");
|
|
47
|
+
const minBundleId = requireRouteParam(params, "minBundleId");
|
|
48
|
+
const bundleId = requireRouteParam(params, "bundleId");
|
|
13
49
|
const updateInfo = await api.getAppUpdateInfo({
|
|
14
50
|
_updateStrategy: "fingerprint",
|
|
15
|
-
platform
|
|
16
|
-
fingerprintHash
|
|
17
|
-
channel
|
|
18
|
-
minBundleId
|
|
19
|
-
bundleId
|
|
20
|
-
|
|
51
|
+
platform,
|
|
52
|
+
fingerprintHash,
|
|
53
|
+
channel,
|
|
54
|
+
minBundleId,
|
|
55
|
+
bundleId,
|
|
56
|
+
cohort: decodeMaybe(params.cohort)
|
|
57
|
+
}, context);
|
|
21
58
|
return new Response(JSON.stringify(updateInfo), {
|
|
22
59
|
status: 200,
|
|
23
60
|
headers: { "Content-Type": "application/json" }
|
|
24
61
|
});
|
|
25
62
|
};
|
|
26
|
-
const
|
|
63
|
+
const handleAppVersionUpdateWithCohort = async (params, _request, api, context) => {
|
|
64
|
+
const platform = requirePlatformParam(params);
|
|
65
|
+
const appVersion = requireRouteParam(params, "appVersion");
|
|
66
|
+
const channel = requireRouteParam(params, "channel");
|
|
67
|
+
const minBundleId = requireRouteParam(params, "minBundleId");
|
|
68
|
+
const bundleId = requireRouteParam(params, "bundleId");
|
|
27
69
|
const updateInfo = await api.getAppUpdateInfo({
|
|
28
70
|
_updateStrategy: "appVersion",
|
|
29
|
-
platform
|
|
30
|
-
appVersion
|
|
31
|
-
channel
|
|
32
|
-
minBundleId
|
|
33
|
-
bundleId
|
|
34
|
-
|
|
71
|
+
platform,
|
|
72
|
+
appVersion,
|
|
73
|
+
channel,
|
|
74
|
+
minBundleId,
|
|
75
|
+
bundleId,
|
|
76
|
+
cohort: decodeMaybe(params.cohort)
|
|
77
|
+
}, context);
|
|
35
78
|
return new Response(JSON.stringify(updateInfo), {
|
|
36
79
|
status: 200,
|
|
37
80
|
headers: { "Content-Type": "application/json" }
|
|
38
81
|
});
|
|
39
82
|
};
|
|
40
|
-
const handleGetBundle = async (params, _request, api) => {
|
|
41
|
-
const
|
|
83
|
+
const handleGetBundle = async (params, _request, api, context) => {
|
|
84
|
+
const bundleId = requireRouteParam(params, "id");
|
|
85
|
+
const bundle = await api.getBundleById(bundleId, context);
|
|
42
86
|
if (!bundle) return new Response(JSON.stringify({ error: "Bundle not found" }), {
|
|
43
87
|
status: 404,
|
|
44
88
|
headers: { "Content-Type": "application/json" }
|
|
@@ -48,12 +92,13 @@ const handleGetBundle = async (params, _request, api) => {
|
|
|
48
92
|
headers: { "Content-Type": "application/json" }
|
|
49
93
|
});
|
|
50
94
|
};
|
|
51
|
-
const handleGetBundles = async (_params, request, api) => {
|
|
95
|
+
const handleGetBundles = async (_params, request, api, context) => {
|
|
52
96
|
const url = new URL(request.url);
|
|
53
97
|
const channel = url.searchParams.get("channel") ?? void 0;
|
|
54
|
-
const platform = url.searchParams.get("platform")
|
|
98
|
+
const platform = url.searchParams.get("platform");
|
|
55
99
|
const limit = Number(url.searchParams.get("limit")) || 50;
|
|
56
100
|
const offset = Number(url.searchParams.get("offset")) || 0;
|
|
101
|
+
if (platform !== null && !isPlatform(platform)) throw new HandlerBadRequestError(`Invalid platform: ${platform}. Expected 'ios' or 'android'.`);
|
|
57
102
|
const result = await api.getBundles({
|
|
58
103
|
where: {
|
|
59
104
|
...channel && { channel },
|
|
@@ -61,30 +106,41 @@ const handleGetBundles = async (_params, request, api) => {
|
|
|
61
106
|
},
|
|
62
107
|
limit,
|
|
63
108
|
offset
|
|
64
|
-
});
|
|
109
|
+
}, context);
|
|
65
110
|
return new Response(JSON.stringify(result.data), {
|
|
66
111
|
status: 200,
|
|
67
112
|
headers: { "Content-Type": "application/json" }
|
|
68
113
|
});
|
|
69
114
|
};
|
|
70
|
-
const handleCreateBundles = async (_params, request, api) => {
|
|
115
|
+
const handleCreateBundles = async (_params, request, api, context) => {
|
|
71
116
|
const body = await request.json();
|
|
72
117
|
const bundles = Array.isArray(body) ? body : [body];
|
|
73
|
-
for (const bundle of bundles) await api.insertBundle(bundle);
|
|
118
|
+
for (const bundle of bundles) await api.insertBundle(bundle, context);
|
|
74
119
|
return new Response(JSON.stringify({ success: true }), {
|
|
75
120
|
status: 201,
|
|
76
121
|
headers: { "Content-Type": "application/json" }
|
|
77
122
|
});
|
|
78
123
|
};
|
|
79
|
-
const
|
|
80
|
-
|
|
124
|
+
const handleUpdateBundle = async (params, request, api, context) => {
|
|
125
|
+
const bundleId = requireRouteParam(params, "id");
|
|
126
|
+
const body = await request.json();
|
|
127
|
+
const bundlePatch = requireBundlePatchPayload(Array.isArray(body) ? body[0] : body, bundleId);
|
|
128
|
+
await api.updateBundleById(bundleId, bundlePatch, context);
|
|
129
|
+
return new Response(JSON.stringify({ success: true }), {
|
|
130
|
+
status: 200,
|
|
131
|
+
headers: { "Content-Type": "application/json" }
|
|
132
|
+
});
|
|
133
|
+
};
|
|
134
|
+
const handleDeleteBundle = async (params, _request, api, context) => {
|
|
135
|
+
const bundleId = requireRouteParam(params, "id");
|
|
136
|
+
await api.deleteBundleById(bundleId, context);
|
|
81
137
|
return new Response(JSON.stringify({ success: true }), {
|
|
82
138
|
status: 200,
|
|
83
139
|
headers: { "Content-Type": "application/json" }
|
|
84
140
|
});
|
|
85
141
|
};
|
|
86
|
-
const handleGetChannels = async (_params, _request, api) => {
|
|
87
|
-
const channels = await api.getChannels();
|
|
142
|
+
const handleGetChannels = async (_params, _request, api, context) => {
|
|
143
|
+
const channels = await api.getChannels(context);
|
|
88
144
|
return new Response(JSON.stringify({ channels }), {
|
|
89
145
|
status: 200,
|
|
90
146
|
headers: { "Content-Type": "application/json" }
|
|
@@ -92,35 +148,45 @@ const handleGetChannels = async (_params, _request, api) => {
|
|
|
92
148
|
};
|
|
93
149
|
const routes = {
|
|
94
150
|
version: handleVersion,
|
|
95
|
-
|
|
96
|
-
|
|
151
|
+
fingerprintUpdateWithCohort: handleFingerprintUpdateWithCohort,
|
|
152
|
+
appVersionUpdateWithCohort: handleAppVersionUpdateWithCohort,
|
|
97
153
|
getBundle: handleGetBundle,
|
|
98
154
|
getBundles: handleGetBundles,
|
|
99
155
|
createBundles: handleCreateBundles,
|
|
156
|
+
updateBundle: handleUpdateBundle,
|
|
100
157
|
deleteBundle: handleDeleteBundle,
|
|
101
158
|
getChannels: handleGetChannels
|
|
102
159
|
};
|
|
103
160
|
/**
|
|
104
161
|
* Creates a Web Standard Request handler for Hot Updater API
|
|
105
|
-
* This handler is framework-agnostic and works with any
|
|
106
|
-
*
|
|
162
|
+
* This handler is framework-agnostic and works with any runtime that
|
|
163
|
+
* supports standard Request/Response objects.
|
|
107
164
|
*/
|
|
108
165
|
function createHandler(api, options = {}) {
|
|
109
166
|
const basePath = options.basePath ?? "/api";
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
167
|
+
const updateCheckEnabled = options.routes?.updateCheck ?? true;
|
|
168
|
+
const bundlesEnabled = options.routes?.bundles ?? true;
|
|
169
|
+
const router = require_internalRouter.createRouter();
|
|
170
|
+
if (updateCheckEnabled) {
|
|
171
|
+
require_internalRouter.addRoute(router, "GET", "/fingerprint/:platform/:fingerprintHash/:channel/:minBundleId/:bundleId", "fingerprintUpdateWithCohort");
|
|
172
|
+
require_internalRouter.addRoute(router, "GET", "/fingerprint/:platform/:fingerprintHash/:channel/:minBundleId/:bundleId/:cohort", "fingerprintUpdateWithCohort");
|
|
173
|
+
require_internalRouter.addRoute(router, "GET", "/app-version/:platform/:appVersion/:channel/:minBundleId/:bundleId", "appVersionUpdateWithCohort");
|
|
174
|
+
require_internalRouter.addRoute(router, "GET", "/app-version/:platform/:appVersion/:channel/:minBundleId/:bundleId/:cohort", "appVersionUpdateWithCohort");
|
|
175
|
+
}
|
|
176
|
+
if (bundlesEnabled) {
|
|
177
|
+
require_internalRouter.addRoute(router, "GET", "/version", "version");
|
|
178
|
+
require_internalRouter.addRoute(router, "GET", "/api/bundles/channels", "getChannels");
|
|
179
|
+
require_internalRouter.addRoute(router, "GET", "/api/bundles/:id", "getBundle");
|
|
180
|
+
require_internalRouter.addRoute(router, "GET", "/api/bundles", "getBundles");
|
|
181
|
+
require_internalRouter.addRoute(router, "POST", "/api/bundles", "createBundles");
|
|
182
|
+
require_internalRouter.addRoute(router, "PATCH", "/api/bundles/:id", "updateBundle");
|
|
183
|
+
require_internalRouter.addRoute(router, "DELETE", "/api/bundles/:id", "deleteBundle");
|
|
184
|
+
}
|
|
185
|
+
return async (request, context) => {
|
|
120
186
|
try {
|
|
121
187
|
const path = new URL(request.url).pathname;
|
|
122
188
|
const method = request.method;
|
|
123
|
-
const match =
|
|
189
|
+
const match = require_internalRouter.findRoute(router, method, path.startsWith(basePath) ? path.slice(basePath.length) : path);
|
|
124
190
|
if (!match) return new Response(JSON.stringify({ error: "Not found" }), {
|
|
125
191
|
status: 404,
|
|
126
192
|
headers: { "Content-Type": "application/json" }
|
|
@@ -130,8 +196,12 @@ function createHandler(api, options = {}) {
|
|
|
130
196
|
status: 500,
|
|
131
197
|
headers: { "Content-Type": "application/json" }
|
|
132
198
|
});
|
|
133
|
-
return await handler(match.params || {}, request, api);
|
|
199
|
+
return await handler(match.params || {}, request, api, context);
|
|
134
200
|
} catch (error) {
|
|
201
|
+
if (error instanceof HandlerBadRequestError) return new Response(JSON.stringify({ error: error.message }), {
|
|
202
|
+
status: 400,
|
|
203
|
+
headers: { "Content-Type": "application/json" }
|
|
204
|
+
});
|
|
135
205
|
console.error("Hot Updater handler error:", error);
|
|
136
206
|
return new Response(JSON.stringify({
|
|
137
207
|
error: "Internal server error",
|
|
@@ -143,6 +213,5 @@ function createHandler(api, options = {}) {
|
|
|
143
213
|
}
|
|
144
214
|
};
|
|
145
215
|
}
|
|
146
|
-
|
|
147
216
|
//#endregion
|
|
148
|
-
exports.createHandler = createHandler;
|
|
217
|
+
exports.createHandler = createHandler;
|
package/dist/handler.d.cts
CHANGED
|
@@ -1,24 +1,19 @@
|
|
|
1
1
|
import { PaginationInfo } from "./types/index.cjs";
|
|
2
|
+
import { DatabaseBundleQueryOptions, HotUpdaterContext } from "@hot-updater/plugin-core";
|
|
2
3
|
import { AppUpdateInfo, AppVersionGetBundlesArgs, Bundle, FingerprintGetBundlesArgs } from "@hot-updater/core";
|
|
3
4
|
|
|
4
5
|
//#region src/handler.d.ts
|
|
5
|
-
interface HandlerAPI {
|
|
6
|
-
getAppUpdateInfo: (args: AppVersionGetBundlesArgs | FingerprintGetBundlesArgs) => Promise<AppUpdateInfo | null>;
|
|
7
|
-
getBundleById: (id: string) => Promise<Bundle | null>;
|
|
8
|
-
getBundles: (options: {
|
|
9
|
-
where?: {
|
|
10
|
-
channel?: string;
|
|
11
|
-
platform?: string;
|
|
12
|
-
};
|
|
13
|
-
limit: number;
|
|
14
|
-
offset: number;
|
|
15
|
-
}) => Promise<{
|
|
6
|
+
interface HandlerAPI<TContext = unknown> {
|
|
7
|
+
getAppUpdateInfo: (args: AppVersionGetBundlesArgs | FingerprintGetBundlesArgs, context?: HotUpdaterContext<TContext>) => Promise<AppUpdateInfo | null>;
|
|
8
|
+
getBundleById: (id: string, context?: HotUpdaterContext<TContext>) => Promise<Bundle | null>;
|
|
9
|
+
getBundles: (options: DatabaseBundleQueryOptions, context?: HotUpdaterContext<TContext>) => Promise<{
|
|
16
10
|
data: Bundle[];
|
|
17
11
|
pagination: PaginationInfo;
|
|
18
12
|
}>;
|
|
19
|
-
insertBundle: (bundle: Bundle) => Promise<void>;
|
|
20
|
-
|
|
21
|
-
|
|
13
|
+
insertBundle: (bundle: Bundle, context?: HotUpdaterContext<TContext>) => Promise<void>;
|
|
14
|
+
updateBundleById: (bundleId: string, bundle: Partial<Bundle>, context?: HotUpdaterContext<TContext>) => Promise<void>;
|
|
15
|
+
deleteBundleById: (bundleId: string, context?: HotUpdaterContext<TContext>) => Promise<void>;
|
|
16
|
+
getChannels: (context?: HotUpdaterContext<TContext>) => Promise<string[]>;
|
|
22
17
|
}
|
|
23
18
|
interface HandlerOptions {
|
|
24
19
|
/**
|
|
@@ -26,12 +21,27 @@ interface HandlerOptions {
|
|
|
26
21
|
* @default "/api"
|
|
27
22
|
*/
|
|
28
23
|
basePath?: string;
|
|
24
|
+
routes?: HandlerRoutes;
|
|
25
|
+
}
|
|
26
|
+
interface HandlerRoutes {
|
|
27
|
+
/**
|
|
28
|
+
* Controls whether update-check routes are mounted.
|
|
29
|
+
* @default true
|
|
30
|
+
*/
|
|
31
|
+
updateCheck?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Controls whether bundle management routes are mounted.
|
|
34
|
+
* This includes `/version` and `/api/bundles*`, which are used by the
|
|
35
|
+
* CLI `standaloneRepository` plugin.
|
|
36
|
+
* @default true
|
|
37
|
+
*/
|
|
38
|
+
bundles?: boolean;
|
|
29
39
|
}
|
|
30
40
|
/**
|
|
31
41
|
* Creates a Web Standard Request handler for Hot Updater API
|
|
32
|
-
* This handler is framework-agnostic and works with any
|
|
33
|
-
*
|
|
42
|
+
* This handler is framework-agnostic and works with any runtime that
|
|
43
|
+
* supports standard Request/Response objects.
|
|
34
44
|
*/
|
|
35
|
-
declare function createHandler(api: HandlerAPI
|
|
45
|
+
declare function createHandler<TContext = unknown>(api: HandlerAPI<TContext>, options?: HandlerOptions): (request: Request, context?: HotUpdaterContext<TContext>) => Promise<Response>;
|
|
36
46
|
//#endregion
|
|
37
|
-
export { HandlerAPI, HandlerOptions, createHandler };
|
|
47
|
+
export { HandlerAPI, HandlerOptions, HandlerRoutes, createHandler };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { PaginationInfo } from "./types/index.mjs";
|
|
2
|
+
import { AppUpdateInfo, AppVersionGetBundlesArgs, Bundle, FingerprintGetBundlesArgs } from "@hot-updater/core";
|
|
3
|
+
import { DatabaseBundleQueryOptions, HotUpdaterContext } from "@hot-updater/plugin-core";
|
|
4
|
+
|
|
5
|
+
//#region src/handler.d.ts
|
|
6
|
+
interface HandlerAPI<TContext = unknown> {
|
|
7
|
+
getAppUpdateInfo: (args: AppVersionGetBundlesArgs | FingerprintGetBundlesArgs, context?: HotUpdaterContext<TContext>) => Promise<AppUpdateInfo | null>;
|
|
8
|
+
getBundleById: (id: string, context?: HotUpdaterContext<TContext>) => Promise<Bundle | null>;
|
|
9
|
+
getBundles: (options: DatabaseBundleQueryOptions, context?: HotUpdaterContext<TContext>) => Promise<{
|
|
10
|
+
data: Bundle[];
|
|
11
|
+
pagination: PaginationInfo;
|
|
12
|
+
}>;
|
|
13
|
+
insertBundle: (bundle: Bundle, context?: HotUpdaterContext<TContext>) => Promise<void>;
|
|
14
|
+
updateBundleById: (bundleId: string, bundle: Partial<Bundle>, context?: HotUpdaterContext<TContext>) => Promise<void>;
|
|
15
|
+
deleteBundleById: (bundleId: string, context?: HotUpdaterContext<TContext>) => Promise<void>;
|
|
16
|
+
getChannels: (context?: HotUpdaterContext<TContext>) => Promise<string[]>;
|
|
17
|
+
}
|
|
18
|
+
interface HandlerOptions {
|
|
19
|
+
/**
|
|
20
|
+
* Base path for all routes
|
|
21
|
+
* @default "/api"
|
|
22
|
+
*/
|
|
23
|
+
basePath?: string;
|
|
24
|
+
routes?: HandlerRoutes;
|
|
25
|
+
}
|
|
26
|
+
interface HandlerRoutes {
|
|
27
|
+
/**
|
|
28
|
+
* Controls whether update-check routes are mounted.
|
|
29
|
+
* @default true
|
|
30
|
+
*/
|
|
31
|
+
updateCheck?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Controls whether bundle management routes are mounted.
|
|
34
|
+
* This includes `/version` and `/api/bundles*`, which are used by the
|
|
35
|
+
* CLI `standaloneRepository` plugin.
|
|
36
|
+
* @default true
|
|
37
|
+
*/
|
|
38
|
+
bundles?: boolean;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Creates a Web Standard Request handler for Hot Updater API
|
|
42
|
+
* This handler is framework-agnostic and works with any runtime that
|
|
43
|
+
* supports standard Request/Response objects.
|
|
44
|
+
*/
|
|
45
|
+
declare function createHandler<TContext = unknown>(api: HandlerAPI<TContext>, options?: HandlerOptions): (request: Request, context?: HotUpdaterContext<TContext>) => Promise<Response>;
|
|
46
|
+
//#endregion
|
|
47
|
+
export { HandlerAPI, HandlerOptions, HandlerRoutes, createHandler };
|