@hot-updater/server 0.29.2 → 0.29.4
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 +4 -1
- package/dist/db/index.mjs +4 -1
- package/dist/db/pluginCore.cjs +28 -14
- package/dist/db/pluginCore.mjs +28 -14
- package/dist/db/types.d.cts +2 -5
- package/dist/db/types.d.mts +2 -5
- package/dist/handler.cjs +4 -4
- package/dist/handler.d.cts +2 -5
- package/dist/handler.d.mts +2 -5
- package/dist/handler.mjs +4 -4
- package/dist/index.d.cts +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/runtime.cjs +4 -2
- package/dist/runtime.mjs +4 -2
- package/dist/types/index.d.cts +9 -3
- package/dist/types/index.d.mts +9 -3
- package/package.json +6 -6
- package/src/db/index.spec.ts +93 -0
- package/src/db/index.ts +19 -4
- package/src/db/ormCore.ts +3 -2
- package/src/db/ormUpdateCheck.bench.ts +1 -0
- package/src/db/pluginCore.ts +43 -17
- package/src/db/pluginUpdateCheck.bench.ts +5 -1
- package/src/db/types.ts +3 -2
- package/src/handler-standalone.integration.spec.ts +25 -1
- package/src/handler.spec.ts +88 -17
- package/src/handler.ts +10 -4
- package/src/runtime.spec.ts +155 -0
- package/src/runtime.ts +10 -8
- package/src/types/index.ts +11 -2
package/dist/db/index.cjs
CHANGED
|
@@ -21,7 +21,10 @@ function createHotUpdater(options) {
|
|
|
21
21
|
return resolveStoragePluginUrl(storageUri, context);
|
|
22
22
|
};
|
|
23
23
|
const database = options.database;
|
|
24
|
-
const core = require_types.isDatabasePluginFactory(database) || require_types.isDatabasePlugin(database) ?
|
|
24
|
+
const core = require_types.isDatabasePluginFactory(database) || require_types.isDatabasePlugin(database) ? (() => {
|
|
25
|
+
const plugin = require_types.isDatabasePluginFactory(database) ? database() : database;
|
|
26
|
+
return require_pluginCore.createPluginDatabaseCore(() => plugin, resolveFileUrl, require_types.isDatabasePluginFactory(database) ? { createMutationPlugin: () => database() } : void 0);
|
|
27
|
+
})() : require_ormCore.createOrmDatabaseCore({
|
|
25
28
|
database,
|
|
26
29
|
resolveFileUrl
|
|
27
30
|
});
|
package/dist/db/index.mjs
CHANGED
|
@@ -21,7 +21,10 @@ function createHotUpdater(options) {
|
|
|
21
21
|
return resolveStoragePluginUrl(storageUri, context);
|
|
22
22
|
};
|
|
23
23
|
const database = options.database;
|
|
24
|
-
const core = isDatabasePluginFactory(database) || isDatabasePlugin(database) ?
|
|
24
|
+
const core = isDatabasePluginFactory(database) || isDatabasePlugin(database) ? (() => {
|
|
25
|
+
const plugin = isDatabasePluginFactory(database) ? database() : database;
|
|
26
|
+
return createPluginDatabaseCore(() => plugin, resolveFileUrl, isDatabasePluginFactory(database) ? { createMutationPlugin: () => database() } : void 0);
|
|
27
|
+
})() : createOrmDatabaseCore({
|
|
25
28
|
database,
|
|
26
29
|
resolveFileUrl
|
|
27
30
|
});
|
package/dist/db/pluginCore.cjs
CHANGED
|
@@ -46,9 +46,17 @@ const INIT_BUNDLE_ROLLBACK_UPDATE_INFO = {
|
|
|
46
46
|
storageUri: null,
|
|
47
47
|
fileHash: null
|
|
48
48
|
};
|
|
49
|
-
function createPluginDatabaseCore(
|
|
49
|
+
function createPluginDatabaseCore(getPlugin, resolveFileUrl, options) {
|
|
50
|
+
const runWithMutationPlugin = async (operation) => {
|
|
51
|
+
const plugin = options?.createMutationPlugin?.() ?? getPlugin();
|
|
52
|
+
try {
|
|
53
|
+
return await operation(plugin);
|
|
54
|
+
} finally {
|
|
55
|
+
if (options?.createMutationPlugin) await options.cleanupMutationPlugin?.(plugin);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
50
58
|
const getSortedBundlePage = async (options, context) => {
|
|
51
|
-
const result = await
|
|
59
|
+
const result = await getPlugin().getBundles({
|
|
52
60
|
...options,
|
|
53
61
|
orderBy: options.orderBy ?? DESC_ORDER
|
|
54
62
|
}, context);
|
|
@@ -102,7 +110,7 @@ function createPluginDatabaseCore(plugin, resolveFileUrl) {
|
|
|
102
110
|
return {
|
|
103
111
|
api: {
|
|
104
112
|
async getBundleById(id, context) {
|
|
105
|
-
return
|
|
113
|
+
return getPlugin().getBundleById(id, context);
|
|
106
114
|
},
|
|
107
115
|
async getUpdateInfo(args, context) {
|
|
108
116
|
const channel = args.channel ?? "production";
|
|
@@ -143,27 +151,33 @@ function createPluginDatabaseCore(plugin, resolveFileUrl) {
|
|
|
143
151
|
};
|
|
144
152
|
},
|
|
145
153
|
async getChannels(context) {
|
|
146
|
-
return
|
|
154
|
+
return getPlugin().getChannels(context);
|
|
147
155
|
},
|
|
148
156
|
async getBundles(options, context) {
|
|
149
|
-
return
|
|
157
|
+
return getPlugin().getBundles(options, context);
|
|
150
158
|
},
|
|
151
159
|
async insertBundle(bundle, context) {
|
|
152
|
-
await
|
|
153
|
-
|
|
160
|
+
await runWithMutationPlugin(async (plugin) => {
|
|
161
|
+
await plugin.appendBundle(bundle, context);
|
|
162
|
+
await plugin.commitBundle(context);
|
|
163
|
+
});
|
|
154
164
|
},
|
|
155
165
|
async updateBundleById(bundleId, newBundle, context) {
|
|
156
|
-
await
|
|
157
|
-
|
|
166
|
+
await runWithMutationPlugin(async (plugin) => {
|
|
167
|
+
await plugin.updateBundle(bundleId, newBundle, context);
|
|
168
|
+
await plugin.commitBundle(context);
|
|
169
|
+
});
|
|
158
170
|
},
|
|
159
171
|
async deleteBundleById(bundleId, context) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
172
|
+
await runWithMutationPlugin(async (plugin) => {
|
|
173
|
+
const bundle = await plugin.getBundleById(bundleId, context);
|
|
174
|
+
if (!bundle) return;
|
|
175
|
+
await plugin.deleteBundle(bundle, context);
|
|
176
|
+
await plugin.commitBundle(context);
|
|
177
|
+
});
|
|
164
178
|
}
|
|
165
179
|
},
|
|
166
|
-
adapterName:
|
|
180
|
+
adapterName: getPlugin().name,
|
|
167
181
|
createMigrator: () => {
|
|
168
182
|
throw new Error("createMigrator is only available for Kysely/Prisma/Drizzle database adapters.");
|
|
169
183
|
},
|
package/dist/db/pluginCore.mjs
CHANGED
|
@@ -46,9 +46,17 @@ const INIT_BUNDLE_ROLLBACK_UPDATE_INFO = {
|
|
|
46
46
|
storageUri: null,
|
|
47
47
|
fileHash: null
|
|
48
48
|
};
|
|
49
|
-
function createPluginDatabaseCore(
|
|
49
|
+
function createPluginDatabaseCore(getPlugin, resolveFileUrl, options) {
|
|
50
|
+
const runWithMutationPlugin = async (operation) => {
|
|
51
|
+
const plugin = options?.createMutationPlugin?.() ?? getPlugin();
|
|
52
|
+
try {
|
|
53
|
+
return await operation(plugin);
|
|
54
|
+
} finally {
|
|
55
|
+
if (options?.createMutationPlugin) await options.cleanupMutationPlugin?.(plugin);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
50
58
|
const getSortedBundlePage = async (options, context) => {
|
|
51
|
-
const result = await
|
|
59
|
+
const result = await getPlugin().getBundles({
|
|
52
60
|
...options,
|
|
53
61
|
orderBy: options.orderBy ?? DESC_ORDER
|
|
54
62
|
}, context);
|
|
@@ -102,7 +110,7 @@ function createPluginDatabaseCore(plugin, resolveFileUrl) {
|
|
|
102
110
|
return {
|
|
103
111
|
api: {
|
|
104
112
|
async getBundleById(id, context) {
|
|
105
|
-
return
|
|
113
|
+
return getPlugin().getBundleById(id, context);
|
|
106
114
|
},
|
|
107
115
|
async getUpdateInfo(args, context) {
|
|
108
116
|
const channel = args.channel ?? "production";
|
|
@@ -143,27 +151,33 @@ function createPluginDatabaseCore(plugin, resolveFileUrl) {
|
|
|
143
151
|
};
|
|
144
152
|
},
|
|
145
153
|
async getChannels(context) {
|
|
146
|
-
return
|
|
154
|
+
return getPlugin().getChannels(context);
|
|
147
155
|
},
|
|
148
156
|
async getBundles(options, context) {
|
|
149
|
-
return
|
|
157
|
+
return getPlugin().getBundles(options, context);
|
|
150
158
|
},
|
|
151
159
|
async insertBundle(bundle, context) {
|
|
152
|
-
await
|
|
153
|
-
|
|
160
|
+
await runWithMutationPlugin(async (plugin) => {
|
|
161
|
+
await plugin.appendBundle(bundle, context);
|
|
162
|
+
await plugin.commitBundle(context);
|
|
163
|
+
});
|
|
154
164
|
},
|
|
155
165
|
async updateBundleById(bundleId, newBundle, context) {
|
|
156
|
-
await
|
|
157
|
-
|
|
166
|
+
await runWithMutationPlugin(async (plugin) => {
|
|
167
|
+
await plugin.updateBundle(bundleId, newBundle, context);
|
|
168
|
+
await plugin.commitBundle(context);
|
|
169
|
+
});
|
|
158
170
|
},
|
|
159
171
|
async deleteBundleById(bundleId, context) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
172
|
+
await runWithMutationPlugin(async (plugin) => {
|
|
173
|
+
const bundle = await plugin.getBundleById(bundleId, context);
|
|
174
|
+
if (!bundle) return;
|
|
175
|
+
await plugin.deleteBundle(bundle, context);
|
|
176
|
+
await plugin.commitBundle(context);
|
|
177
|
+
});
|
|
164
178
|
}
|
|
165
179
|
},
|
|
166
|
-
adapterName:
|
|
180
|
+
adapterName: getPlugin().name,
|
|
167
181
|
createMigrator: () => {
|
|
168
182
|
throw new Error("createMigrator is only available for Kysely/Prisma/Drizzle database adapters.");
|
|
169
183
|
},
|
package/dist/db/types.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { PaginatedResult } from "../types/index.cjs";
|
|
2
2
|
import { DatabaseBundleQueryOptions, DatabasePlugin, HotUpdaterContext, StoragePlugin } from "@hot-updater/plugin-core";
|
|
3
3
|
import { AppUpdateInfo, Bundle, GetBundlesArgs, UpdateInfo } from "@hot-updater/core";
|
|
4
4
|
import { FumaDBAdapter } from "fumadb/adapters";
|
|
@@ -11,10 +11,7 @@ interface DatabaseAPI<TContext = unknown> {
|
|
|
11
11
|
getUpdateInfo(args: GetBundlesArgs, context?: HotUpdaterContext<TContext>): Promise<UpdateInfo | null>;
|
|
12
12
|
getAppUpdateInfo(args: GetBundlesArgs, context?: HotUpdaterContext<TContext>): Promise<AppUpdateInfo | null>;
|
|
13
13
|
getChannels(context?: HotUpdaterContext<TContext>): Promise<string[]>;
|
|
14
|
-
getBundles(options: DatabaseBundleQueryOptions, context?: HotUpdaterContext<TContext>): Promise<
|
|
15
|
-
data: Bundle[];
|
|
16
|
-
pagination: PaginationInfo;
|
|
17
|
-
}>;
|
|
14
|
+
getBundles(options: DatabaseBundleQueryOptions, context?: HotUpdaterContext<TContext>): Promise<PaginatedResult>;
|
|
18
15
|
insertBundle(bundle: Bundle, context?: HotUpdaterContext<TContext>): Promise<void>;
|
|
19
16
|
updateBundleById(bundleId: string, newBundle: Partial<Bundle>, context?: HotUpdaterContext<TContext>): Promise<void>;
|
|
20
17
|
deleteBundleById(bundleId: string, context?: HotUpdaterContext<TContext>): Promise<void>;
|
package/dist/db/types.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { PaginatedResult } from "../types/index.mjs";
|
|
2
2
|
import { AppUpdateInfo, Bundle, GetBundlesArgs, UpdateInfo } from "@hot-updater/core";
|
|
3
3
|
import { DatabaseBundleQueryOptions, DatabasePlugin, HotUpdaterContext, StoragePlugin } from "@hot-updater/plugin-core";
|
|
4
4
|
import { FumaDBAdapter } from "fumadb/adapters";
|
|
@@ -11,10 +11,7 @@ interface DatabaseAPI<TContext = unknown> {
|
|
|
11
11
|
getUpdateInfo(args: GetBundlesArgs, context?: HotUpdaterContext<TContext>): Promise<UpdateInfo | null>;
|
|
12
12
|
getAppUpdateInfo(args: GetBundlesArgs, context?: HotUpdaterContext<TContext>): Promise<AppUpdateInfo | null>;
|
|
13
13
|
getChannels(context?: HotUpdaterContext<TContext>): Promise<string[]>;
|
|
14
|
-
getBundles(options: DatabaseBundleQueryOptions, context?: HotUpdaterContext<TContext>): Promise<
|
|
15
|
-
data: Bundle[];
|
|
16
|
-
pagination: PaginationInfo;
|
|
17
|
-
}>;
|
|
14
|
+
getBundles(options: DatabaseBundleQueryOptions, context?: HotUpdaterContext<TContext>): Promise<PaginatedResult>;
|
|
18
15
|
insertBundle(bundle: Bundle, context?: HotUpdaterContext<TContext>): Promise<void>;
|
|
19
16
|
updateBundleById(bundleId: string, newBundle: Partial<Bundle>, context?: HotUpdaterContext<TContext>): Promise<void>;
|
|
20
17
|
deleteBundleById(bundleId: string, context?: HotUpdaterContext<TContext>): Promise<void>;
|
package/dist/handler.cjs
CHANGED
|
@@ -7,7 +7,7 @@ var HandlerBadRequestError = class extends Error {
|
|
|
7
7
|
}
|
|
8
8
|
};
|
|
9
9
|
const handleVersion = async () => {
|
|
10
|
-
return new Response(JSON.stringify({ version: "0.29.
|
|
10
|
+
return new Response(JSON.stringify({ version: "0.29.4" }), {
|
|
11
11
|
status: 200,
|
|
12
12
|
headers: { "Content-Type": "application/json" }
|
|
13
13
|
});
|
|
@@ -107,7 +107,7 @@ const handleGetBundles = async (_params, request, api, context) => {
|
|
|
107
107
|
limit,
|
|
108
108
|
offset
|
|
109
109
|
}, context);
|
|
110
|
-
return new Response(JSON.stringify(result
|
|
110
|
+
return new Response(JSON.stringify(result), {
|
|
111
111
|
status: 200,
|
|
112
112
|
headers: { "Content-Type": "application/json" }
|
|
113
113
|
});
|
|
@@ -140,8 +140,8 @@ const handleDeleteBundle = async (params, _request, api, context) => {
|
|
|
140
140
|
});
|
|
141
141
|
};
|
|
142
142
|
const handleGetChannels = async (_params, _request, api, context) => {
|
|
143
|
-
const
|
|
144
|
-
return new Response(JSON.stringify(
|
|
143
|
+
const response = { data: { channels: await api.getChannels(context) } };
|
|
144
|
+
return new Response(JSON.stringify(response), {
|
|
145
145
|
status: 200,
|
|
146
146
|
headers: { "Content-Type": "application/json" }
|
|
147
147
|
});
|
package/dist/handler.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { PaginatedResult } from "./types/index.cjs";
|
|
2
2
|
import { DatabaseBundleQueryOptions, HotUpdaterContext } from "@hot-updater/plugin-core";
|
|
3
3
|
import { AppUpdateInfo, AppVersionGetBundlesArgs, Bundle, FingerprintGetBundlesArgs } from "@hot-updater/core";
|
|
4
4
|
|
|
@@ -6,10 +6,7 @@ import { AppUpdateInfo, AppVersionGetBundlesArgs, Bundle, FingerprintGetBundlesA
|
|
|
6
6
|
interface HandlerAPI<TContext = unknown> {
|
|
7
7
|
getAppUpdateInfo: (args: AppVersionGetBundlesArgs | FingerprintGetBundlesArgs, context?: HotUpdaterContext<TContext>) => Promise<AppUpdateInfo | null>;
|
|
8
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
|
-
}>;
|
|
9
|
+
getBundles: (options: DatabaseBundleQueryOptions, context?: HotUpdaterContext<TContext>) => Promise<PaginatedResult>;
|
|
13
10
|
insertBundle: (bundle: Bundle, context?: HotUpdaterContext<TContext>) => Promise<void>;
|
|
14
11
|
updateBundleById: (bundleId: string, bundle: Partial<Bundle>, context?: HotUpdaterContext<TContext>) => Promise<void>;
|
|
15
12
|
deleteBundleById: (bundleId: string, context?: HotUpdaterContext<TContext>) => Promise<void>;
|
package/dist/handler.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { PaginatedResult } from "./types/index.mjs";
|
|
2
2
|
import { AppUpdateInfo, AppVersionGetBundlesArgs, Bundle, FingerprintGetBundlesArgs } from "@hot-updater/core";
|
|
3
3
|
import { DatabaseBundleQueryOptions, HotUpdaterContext } from "@hot-updater/plugin-core";
|
|
4
4
|
|
|
@@ -6,10 +6,7 @@ import { DatabaseBundleQueryOptions, HotUpdaterContext } from "@hot-updater/plug
|
|
|
6
6
|
interface HandlerAPI<TContext = unknown> {
|
|
7
7
|
getAppUpdateInfo: (args: AppVersionGetBundlesArgs | FingerprintGetBundlesArgs, context?: HotUpdaterContext<TContext>) => Promise<AppUpdateInfo | null>;
|
|
8
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
|
-
}>;
|
|
9
|
+
getBundles: (options: DatabaseBundleQueryOptions, context?: HotUpdaterContext<TContext>) => Promise<PaginatedResult>;
|
|
13
10
|
insertBundle: (bundle: Bundle, context?: HotUpdaterContext<TContext>) => Promise<void>;
|
|
14
11
|
updateBundleById: (bundleId: string, bundle: Partial<Bundle>, context?: HotUpdaterContext<TContext>) => Promise<void>;
|
|
15
12
|
deleteBundleById: (bundleId: string, context?: HotUpdaterContext<TContext>) => Promise<void>;
|
package/dist/handler.mjs
CHANGED
|
@@ -7,7 +7,7 @@ var HandlerBadRequestError = class extends Error {
|
|
|
7
7
|
}
|
|
8
8
|
};
|
|
9
9
|
const handleVersion = async () => {
|
|
10
|
-
return new Response(JSON.stringify({ version: "0.29.
|
|
10
|
+
return new Response(JSON.stringify({ version: "0.29.4" }), {
|
|
11
11
|
status: 200,
|
|
12
12
|
headers: { "Content-Type": "application/json" }
|
|
13
13
|
});
|
|
@@ -107,7 +107,7 @@ const handleGetBundles = async (_params, request, api, context) => {
|
|
|
107
107
|
limit,
|
|
108
108
|
offset
|
|
109
109
|
}, context);
|
|
110
|
-
return new Response(JSON.stringify(result
|
|
110
|
+
return new Response(JSON.stringify(result), {
|
|
111
111
|
status: 200,
|
|
112
112
|
headers: { "Content-Type": "application/json" }
|
|
113
113
|
});
|
|
@@ -140,8 +140,8 @@ const handleDeleteBundle = async (params, _request, api, context) => {
|
|
|
140
140
|
});
|
|
141
141
|
};
|
|
142
142
|
const handleGetChannels = async (_params, _request, api, context) => {
|
|
143
|
-
const
|
|
144
|
-
return new Response(JSON.stringify(
|
|
143
|
+
const response = { data: { channels: await api.getChannels(context) } };
|
|
144
|
+
return new Response(JSON.stringify(response), {
|
|
145
145
|
status: 200,
|
|
146
146
|
headers: { "Content-Type": "application/json" }
|
|
147
147
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Bundle, PaginatedResult, PaginationInfo, PaginationOptions } from "./types/index.cjs";
|
|
1
|
+
import { Bundle, ChannelsResponse, DataResponse, Paginated, PaginatedResult, PaginationInfo, PaginationOptions } from "./types/index.cjs";
|
|
2
2
|
import { HandlerAPI, HandlerOptions, HandlerRoutes, createHandler } from "./handler.cjs";
|
|
3
3
|
import { HotUpdaterClient, HotUpdaterDB, Migrator } from "./db/ormCore.cjs";
|
|
4
4
|
import { CreateHotUpdaterOptions, HotUpdaterAPI, createHotUpdater } from "./db/index.cjs";
|
|
5
|
-
export { Bundle, CreateHotUpdaterOptions, HandlerAPI, HandlerOptions, HandlerRoutes, HotUpdaterAPI, HotUpdaterClient, HotUpdaterDB, Migrator, PaginatedResult, PaginationInfo, PaginationOptions, createHandler, createHotUpdater };
|
|
5
|
+
export { Bundle, ChannelsResponse, CreateHotUpdaterOptions, DataResponse, HandlerAPI, HandlerOptions, HandlerRoutes, HotUpdaterAPI, HotUpdaterClient, HotUpdaterDB, Migrator, Paginated, PaginatedResult, PaginationInfo, PaginationOptions, createHandler, createHotUpdater };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Bundle, PaginatedResult, PaginationInfo, PaginationOptions } from "./types/index.mjs";
|
|
1
|
+
import { Bundle, ChannelsResponse, DataResponse, Paginated, PaginatedResult, PaginationInfo, PaginationOptions } from "./types/index.mjs";
|
|
2
2
|
import { HandlerAPI, HandlerOptions, HandlerRoutes, createHandler } from "./handler.mjs";
|
|
3
3
|
import { HotUpdaterClient, HotUpdaterDB, Migrator } from "./db/ormCore.mjs";
|
|
4
4
|
import { CreateHotUpdaterOptions, HotUpdaterAPI, createHotUpdater } from "./db/index.mjs";
|
|
5
|
-
export { Bundle, CreateHotUpdaterOptions, HandlerAPI, HandlerOptions, HandlerRoutes, HotUpdaterAPI, HotUpdaterClient, HotUpdaterDB, Migrator, PaginatedResult, PaginationInfo, PaginationOptions, createHandler, createHotUpdater };
|
|
5
|
+
export { Bundle, ChannelsResponse, CreateHotUpdaterOptions, DataResponse, HandlerAPI, HandlerOptions, HandlerRoutes, HotUpdaterAPI, HotUpdaterClient, HotUpdaterDB, Migrator, Paginated, PaginatedResult, PaginationInfo, PaginationOptions, createHandler, createHotUpdater };
|
package/dist/runtime.cjs
CHANGED
|
@@ -5,6 +5,7 @@ const require_pluginCore = require("./db/pluginCore.cjs");
|
|
|
5
5
|
const require_types = require("./db/types.cjs");
|
|
6
6
|
//#region src/runtime.ts
|
|
7
7
|
function createHotUpdater(options) {
|
|
8
|
+
const database = options.database;
|
|
8
9
|
const basePath = require_route.normalizeBasePath(options.basePath ?? "/api");
|
|
9
10
|
const storagePlugins = (options.storages ?? options.storagePlugins ?? []).map((plugin) => typeof plugin === "function" ? plugin() : plugin);
|
|
10
11
|
const resolveStoragePluginUrl = async (storageUri, context) => {
|
|
@@ -17,8 +18,9 @@ function createHotUpdater(options) {
|
|
|
17
18
|
if (!fileUrl) throw new Error("Storage plugin returned empty fileUrl");
|
|
18
19
|
return fileUrl;
|
|
19
20
|
};
|
|
20
|
-
if (!require_types.isDatabasePluginFactory(
|
|
21
|
-
const
|
|
21
|
+
if (!require_types.isDatabasePluginFactory(database) && !require_types.isDatabasePlugin(database)) throw new Error("@hot-updater/server/runtime only supports database plugins.");
|
|
22
|
+
const plugin = require_types.isDatabasePluginFactory(database) ? database() : database;
|
|
23
|
+
const core = require_pluginCore.createPluginDatabaseCore(() => plugin, resolveStoragePluginUrl, require_types.isDatabasePluginFactory(database) ? { createMutationPlugin: () => database() } : void 0);
|
|
22
24
|
const api = {
|
|
23
25
|
...core.api,
|
|
24
26
|
handler: require_handler.createHandler(core.api, {
|
package/dist/runtime.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { createPluginDatabaseCore } from "./db/pluginCore.mjs";
|
|
|
4
4
|
import { isDatabasePlugin, isDatabasePluginFactory } from "./db/types.mjs";
|
|
5
5
|
//#region src/runtime.ts
|
|
6
6
|
function createHotUpdater(options) {
|
|
7
|
+
const database = options.database;
|
|
7
8
|
const basePath = normalizeBasePath(options.basePath ?? "/api");
|
|
8
9
|
const storagePlugins = (options.storages ?? options.storagePlugins ?? []).map((plugin) => typeof plugin === "function" ? plugin() : plugin);
|
|
9
10
|
const resolveStoragePluginUrl = async (storageUri, context) => {
|
|
@@ -16,8 +17,9 @@ function createHotUpdater(options) {
|
|
|
16
17
|
if (!fileUrl) throw new Error("Storage plugin returned empty fileUrl");
|
|
17
18
|
return fileUrl;
|
|
18
19
|
};
|
|
19
|
-
if (!isDatabasePluginFactory(
|
|
20
|
-
const
|
|
20
|
+
if (!isDatabasePluginFactory(database) && !isDatabasePlugin(database)) throw new Error("@hot-updater/server/runtime only supports database plugins.");
|
|
21
|
+
const plugin = isDatabasePluginFactory(database) ? database() : database;
|
|
22
|
+
const core = createPluginDatabaseCore(() => plugin, resolveStoragePluginUrl, isDatabasePluginFactory(database) ? { createMutationPlugin: () => database() } : void 0);
|
|
21
23
|
const api = {
|
|
22
24
|
...core.api,
|
|
23
25
|
handler: createHandler(core.api, {
|
package/dist/types/index.d.cts
CHANGED
|
@@ -13,9 +13,15 @@ interface PaginationOptions {
|
|
|
13
13
|
limit: number;
|
|
14
14
|
offset: number;
|
|
15
15
|
}
|
|
16
|
-
interface
|
|
17
|
-
data:
|
|
16
|
+
interface DataResponse<TData> {
|
|
17
|
+
data: TData;
|
|
18
|
+
}
|
|
19
|
+
interface Paginated<TData> extends DataResponse<TData> {
|
|
18
20
|
pagination: PaginationInfo;
|
|
19
21
|
}
|
|
22
|
+
type PaginatedResult = Paginated<Bundle[]>;
|
|
23
|
+
type ChannelsResponse = DataResponse<{
|
|
24
|
+
channels: string[];
|
|
25
|
+
}>;
|
|
20
26
|
//#endregion
|
|
21
|
-
export { type Bundle$1 as Bundle, PaginatedResult, PaginationInfo, PaginationOptions };
|
|
27
|
+
export { type Bundle$1 as Bundle, ChannelsResponse, DataResponse, Paginated, PaginatedResult, PaginationInfo, PaginationOptions };
|
package/dist/types/index.d.mts
CHANGED
|
@@ -13,9 +13,15 @@ interface PaginationOptions {
|
|
|
13
13
|
limit: number;
|
|
14
14
|
offset: number;
|
|
15
15
|
}
|
|
16
|
-
interface
|
|
17
|
-
data:
|
|
16
|
+
interface DataResponse<TData> {
|
|
17
|
+
data: TData;
|
|
18
|
+
}
|
|
19
|
+
interface Paginated<TData> extends DataResponse<TData> {
|
|
18
20
|
pagination: PaginationInfo;
|
|
19
21
|
}
|
|
22
|
+
type PaginatedResult = Paginated<Bundle[]>;
|
|
23
|
+
type ChannelsResponse = DataResponse<{
|
|
24
|
+
channels: string[];
|
|
25
|
+
}>;
|
|
20
26
|
//#endregion
|
|
21
|
-
export { type Bundle$1 as Bundle, PaginatedResult, PaginationInfo, PaginationOptions };
|
|
27
|
+
export { type Bundle$1 as Bundle, ChannelsResponse, DataResponse, Paginated, PaginatedResult, PaginationInfo, PaginationOptions };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hot-updater/server",
|
|
3
|
-
"version": "0.29.
|
|
3
|
+
"version": "0.29.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "React Native OTA solution for self-hosted",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -53,9 +53,9 @@
|
|
|
53
53
|
"fumadb": "0.2.2",
|
|
54
54
|
"rou3": "0.7.9",
|
|
55
55
|
"semver": "^7.7.2",
|
|
56
|
-
"@hot-updater/plugin-core": "0.29.
|
|
57
|
-
"@hot-updater/core": "0.29.
|
|
58
|
-
"@hot-updater/js": "0.29.
|
|
56
|
+
"@hot-updater/plugin-core": "0.29.4",
|
|
57
|
+
"@hot-updater/core": "0.29.4",
|
|
58
|
+
"@hot-updater/js": "0.29.4"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@electric-sql/pglite": "^0.2.17",
|
|
@@ -66,8 +66,8 @@
|
|
|
66
66
|
"kysely-pglite-dialect": "^1.2.0",
|
|
67
67
|
"msw": "^2.7.0",
|
|
68
68
|
"uuidv7": "^1.0.2",
|
|
69
|
-
"@hot-updater/standalone": "0.29.
|
|
70
|
-
"@hot-updater/test-utils": "0.29.
|
|
69
|
+
"@hot-updater/standalone": "0.29.4",
|
|
70
|
+
"@hot-updater/test-utils": "0.29.4"
|
|
71
71
|
},
|
|
72
72
|
"scripts": {
|
|
73
73
|
"build": "tsdown",
|
package/src/db/index.spec.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
StoragePlugin,
|
|
6
6
|
StorageResolveContext,
|
|
7
7
|
} from "@hot-updater/plugin-core";
|
|
8
|
+
import { createDatabasePlugin } from "@hot-updater/plugin-core";
|
|
8
9
|
import {
|
|
9
10
|
setupBundleMethodsTestSuite,
|
|
10
11
|
setupGetUpdateInfoTestSuite,
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
it,
|
|
22
23
|
vi,
|
|
23
24
|
} from "vitest";
|
|
25
|
+
|
|
24
26
|
import { kyselyAdapter } from "../adapters/kysely";
|
|
25
27
|
import { createHotUpdater } from "./index";
|
|
26
28
|
|
|
@@ -361,4 +363,95 @@ describe("server/db hotUpdater getUpdateInfo (PGlite + Kysely)", async () => {
|
|
|
361
363
|
);
|
|
362
364
|
});
|
|
363
365
|
});
|
|
366
|
+
|
|
367
|
+
describe("database plugin factories", () => {
|
|
368
|
+
it("isolates pending mutation state between overlapping writes", async () => {
|
|
369
|
+
const committedBundleIds: string[][] = [];
|
|
370
|
+
const onUnmount = vi.fn(async () => undefined);
|
|
371
|
+
let releaseFirstCommit!: () => void;
|
|
372
|
+
let notifyFirstCommitStarted!: () => void;
|
|
373
|
+
const firstCommitStarted = new Promise<void>((resolve) => {
|
|
374
|
+
notifyFirstCommitStarted = resolve;
|
|
375
|
+
});
|
|
376
|
+
const firstCommitGate = new Promise<void>((resolve) => {
|
|
377
|
+
releaseFirstCommit = resolve;
|
|
378
|
+
});
|
|
379
|
+
let commitCount = 0;
|
|
380
|
+
|
|
381
|
+
const isolatedHotUpdater = createHotUpdater({
|
|
382
|
+
database: createDatabasePlugin({
|
|
383
|
+
name: "isolatedPlugin",
|
|
384
|
+
factory: () => ({
|
|
385
|
+
async getBundleById() {
|
|
386
|
+
return null;
|
|
387
|
+
},
|
|
388
|
+
async getBundles() {
|
|
389
|
+
return {
|
|
390
|
+
data: [],
|
|
391
|
+
pagination: {
|
|
392
|
+
hasNextPage: false,
|
|
393
|
+
hasPreviousPage: false,
|
|
394
|
+
currentPage: 1,
|
|
395
|
+
totalPages: 1,
|
|
396
|
+
total: 0,
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
},
|
|
400
|
+
async getChannels() {
|
|
401
|
+
return [];
|
|
402
|
+
},
|
|
403
|
+
onUnmount,
|
|
404
|
+
async commitBundle({ changedSets }) {
|
|
405
|
+
commitCount += 1;
|
|
406
|
+
committedBundleIds.push(
|
|
407
|
+
changedSets.map((change) => change.data.id),
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
if (commitCount === 1) {
|
|
411
|
+
notifyFirstCommitStarted();
|
|
412
|
+
await firstCommitGate;
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
}),
|
|
416
|
+
})({}),
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
const firstBundleId = "00000000-0000-0000-0000-000000000030";
|
|
420
|
+
const secondBundleId = "00000000-0000-0000-0000-000000000031";
|
|
421
|
+
|
|
422
|
+
const firstInsert = isolatedHotUpdater.insertBundle({
|
|
423
|
+
id: firstBundleId,
|
|
424
|
+
platform: "ios",
|
|
425
|
+
shouldForceUpdate: false,
|
|
426
|
+
enabled: true,
|
|
427
|
+
fileHash: "hash-1",
|
|
428
|
+
gitCommitHash: null,
|
|
429
|
+
message: "first bundle",
|
|
430
|
+
channel: "production",
|
|
431
|
+
storageUri: "s3://test-bucket/first.zip",
|
|
432
|
+
targetAppVersion: "1.0.0",
|
|
433
|
+
fingerprintHash: null,
|
|
434
|
+
});
|
|
435
|
+
await firstCommitStarted;
|
|
436
|
+
|
|
437
|
+
const secondInsert = isolatedHotUpdater.insertBundle({
|
|
438
|
+
id: secondBundleId,
|
|
439
|
+
platform: "ios",
|
|
440
|
+
shouldForceUpdate: false,
|
|
441
|
+
enabled: true,
|
|
442
|
+
fileHash: "hash-2",
|
|
443
|
+
gitCommitHash: null,
|
|
444
|
+
message: "second bundle",
|
|
445
|
+
channel: "production",
|
|
446
|
+
storageUri: "s3://test-bucket/second.zip",
|
|
447
|
+
targetAppVersion: "1.0.0",
|
|
448
|
+
fingerprintHash: null,
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
releaseFirstCommit();
|
|
452
|
+
await Promise.all([firstInsert, secondInsert]);
|
|
453
|
+
|
|
454
|
+
expect(committedBundleIds).toEqual([[firstBundleId], [secondBundleId]]);
|
|
455
|
+
});
|
|
456
|
+
});
|
|
364
457
|
});
|
package/src/db/index.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
DatabasePlugin,
|
|
2
3
|
HotUpdaterContext,
|
|
3
4
|
StoragePlugin,
|
|
4
5
|
} from "@hot-updater/plugin-core";
|
|
6
|
+
|
|
5
7
|
import { createHandler, type HandlerRoutes } from "../handler";
|
|
6
8
|
import { normalizeBasePath } from "../route";
|
|
7
9
|
import {
|
|
@@ -92,10 +94,23 @@ export function createHotUpdater<TContext = unknown>(
|
|
|
92
94
|
|
|
93
95
|
const core =
|
|
94
96
|
isDatabasePluginFactory(database) || isDatabasePlugin(database)
|
|
95
|
-
?
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
? (() => {
|
|
98
|
+
const plugin: DatabasePlugin<TContext> = isDatabasePluginFactory(
|
|
99
|
+
database,
|
|
100
|
+
)
|
|
101
|
+
? database()
|
|
102
|
+
: database;
|
|
103
|
+
|
|
104
|
+
return createPluginDatabaseCore<TContext>(
|
|
105
|
+
() => plugin,
|
|
106
|
+
resolveFileUrl,
|
|
107
|
+
isDatabasePluginFactory(database)
|
|
108
|
+
? {
|
|
109
|
+
createMutationPlugin: () => database(),
|
|
110
|
+
}
|
|
111
|
+
: undefined,
|
|
112
|
+
);
|
|
113
|
+
})()
|
|
99
114
|
: createOrmDatabaseCore<TContext>({
|
|
100
115
|
database,
|
|
101
116
|
resolveFileUrl,
|
package/src/db/ormCore.ts
CHANGED
|
@@ -21,10 +21,11 @@ import { semverSatisfies } from "@hot-updater/plugin-core";
|
|
|
21
21
|
import type { InferFumaDB } from "fumadb";
|
|
22
22
|
import { fumadb } from "fumadb";
|
|
23
23
|
import type { FumaDBAdapter } from "fumadb/adapters";
|
|
24
|
+
|
|
24
25
|
import { calculatePagination } from "../calculatePagination";
|
|
25
26
|
import { v0_21_0 } from "../schema/v0_21_0";
|
|
26
27
|
import { v0_29_0 } from "../schema/v0_29_0";
|
|
27
|
-
import type {
|
|
28
|
+
import type { Paginated } from "../types";
|
|
28
29
|
import type { DatabaseAPI } from "./types";
|
|
29
30
|
|
|
30
31
|
const parseTargetCohorts = (value: unknown): string[] | null => {
|
|
@@ -508,7 +509,7 @@ export function createOrmDatabaseCore<TContext = unknown>({
|
|
|
508
509
|
|
|
509
510
|
async getBundles(
|
|
510
511
|
options: DatabaseBundleQueryOptions,
|
|
511
|
-
): Promise<
|
|
512
|
+
): Promise<Paginated<Bundle[]>> {
|
|
512
513
|
const orm = await ensureORM();
|
|
513
514
|
const { where, limit, offset, orderBy } = options;
|
|
514
515
|
|
|
@@ -2,6 +2,7 @@ import { PGlite } from "@electric-sql/pglite";
|
|
|
2
2
|
import { Kysely } from "kysely";
|
|
3
3
|
import { PGliteDialect } from "kysely-pglite-dialect";
|
|
4
4
|
import { bench, describe } from "vitest";
|
|
5
|
+
|
|
5
6
|
import { filterCompatibleAppVersions } from "../../../../plugins/plugin-core/src";
|
|
6
7
|
import type {
|
|
7
8
|
AppVersionGetBundlesArgs,
|
package/src/db/pluginCore.ts
CHANGED
|
@@ -16,10 +16,10 @@ import {
|
|
|
16
16
|
type HotUpdaterContext,
|
|
17
17
|
semverSatisfies,
|
|
18
18
|
} from "@hot-updater/plugin-core";
|
|
19
|
+
|
|
19
20
|
import type { DatabaseAPI } from "./types";
|
|
20
21
|
|
|
21
22
|
const PAGE_SIZE = 100;
|
|
22
|
-
|
|
23
23
|
const DESC_ORDER = { field: "id", direction: "desc" } as const;
|
|
24
24
|
|
|
25
25
|
const bundleMatchesQueryWhere = (
|
|
@@ -100,22 +100,42 @@ const INIT_BUNDLE_ROLLBACK_UPDATE_INFO: UpdateInfo = {
|
|
|
100
100
|
};
|
|
101
101
|
|
|
102
102
|
export function createPluginDatabaseCore<TContext = unknown>(
|
|
103
|
-
|
|
103
|
+
getPlugin: () => DatabasePlugin<TContext>,
|
|
104
104
|
resolveFileUrl: (
|
|
105
105
|
storageUri: string | null,
|
|
106
106
|
context?: HotUpdaterContext<TContext>,
|
|
107
107
|
) => Promise<string | null>,
|
|
108
|
+
options?: {
|
|
109
|
+
createMutationPlugin?: () => DatabasePlugin<TContext>;
|
|
110
|
+
cleanupMutationPlugin?: (
|
|
111
|
+
plugin: DatabasePlugin<TContext>,
|
|
112
|
+
) => Promise<void> | void;
|
|
113
|
+
},
|
|
108
114
|
): {
|
|
109
115
|
api: DatabaseAPI<TContext>;
|
|
110
116
|
adapterName: string;
|
|
111
117
|
createMigrator: () => never;
|
|
112
118
|
generateSchema: () => never;
|
|
113
119
|
} {
|
|
120
|
+
const runWithMutationPlugin = async <T>(
|
|
121
|
+
operation: (plugin: DatabasePlugin<TContext>) => Promise<T>,
|
|
122
|
+
): Promise<T> => {
|
|
123
|
+
const plugin = options?.createMutationPlugin?.() ?? getPlugin();
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
return await operation(plugin);
|
|
127
|
+
} finally {
|
|
128
|
+
if (options?.createMutationPlugin) {
|
|
129
|
+
await options.cleanupMutationPlugin?.(plugin);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
114
134
|
const getSortedBundlePage = async (
|
|
115
135
|
options: DatabaseBundleQueryOptions,
|
|
116
136
|
context?: HotUpdaterContext<TContext>,
|
|
117
137
|
): Promise<Awaited<ReturnType<DatabasePlugin<TContext>["getBundles"]>>> => {
|
|
118
|
-
const result = await
|
|
138
|
+
const result = await getPlugin().getBundles(
|
|
119
139
|
{
|
|
120
140
|
...options,
|
|
121
141
|
orderBy: options.orderBy ?? DESC_ORDER,
|
|
@@ -242,7 +262,7 @@ export function createPluginDatabaseCore<TContext = unknown>(
|
|
|
242
262
|
id: string,
|
|
243
263
|
context?: HotUpdaterContext<TContext>,
|
|
244
264
|
): Promise<Bundle | null> {
|
|
245
|
-
return
|
|
265
|
+
return getPlugin().getBundleById(id, context);
|
|
246
266
|
},
|
|
247
267
|
|
|
248
268
|
async getUpdateInfo(
|
|
@@ -314,19 +334,21 @@ export function createPluginDatabaseCore<TContext = unknown>(
|
|
|
314
334
|
async getChannels(
|
|
315
335
|
context?: HotUpdaterContext<TContext>,
|
|
316
336
|
): Promise<string[]> {
|
|
317
|
-
return
|
|
337
|
+
return getPlugin().getChannels(context);
|
|
318
338
|
},
|
|
319
339
|
|
|
320
340
|
async getBundles(options, context?: HotUpdaterContext<TContext>) {
|
|
321
|
-
return
|
|
341
|
+
return getPlugin().getBundles(options, context);
|
|
322
342
|
},
|
|
323
343
|
|
|
324
344
|
async insertBundle(
|
|
325
345
|
bundle: Bundle,
|
|
326
346
|
context?: HotUpdaterContext<TContext>,
|
|
327
347
|
): Promise<void> {
|
|
328
|
-
await
|
|
329
|
-
|
|
348
|
+
await runWithMutationPlugin(async (plugin) => {
|
|
349
|
+
await plugin.appendBundle(bundle, context);
|
|
350
|
+
await plugin.commitBundle(context);
|
|
351
|
+
});
|
|
330
352
|
},
|
|
331
353
|
|
|
332
354
|
async updateBundleById(
|
|
@@ -334,26 +356,30 @@ export function createPluginDatabaseCore<TContext = unknown>(
|
|
|
334
356
|
newBundle: Partial<Bundle>,
|
|
335
357
|
context?: HotUpdaterContext<TContext>,
|
|
336
358
|
): Promise<void> {
|
|
337
|
-
await
|
|
338
|
-
|
|
359
|
+
await runWithMutationPlugin(async (plugin) => {
|
|
360
|
+
await plugin.updateBundle(bundleId, newBundle, context);
|
|
361
|
+
await plugin.commitBundle(context);
|
|
362
|
+
});
|
|
339
363
|
},
|
|
340
364
|
|
|
341
365
|
async deleteBundleById(
|
|
342
366
|
bundleId: string,
|
|
343
367
|
context?: HotUpdaterContext<TContext>,
|
|
344
368
|
): Promise<void> {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
369
|
+
await runWithMutationPlugin(async (plugin) => {
|
|
370
|
+
const bundle = await plugin.getBundleById(bundleId, context);
|
|
371
|
+
if (!bundle) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
await plugin.deleteBundle(bundle, context);
|
|
375
|
+
await plugin.commitBundle(context);
|
|
376
|
+
});
|
|
351
377
|
},
|
|
352
378
|
};
|
|
353
379
|
|
|
354
380
|
return {
|
|
355
381
|
api,
|
|
356
|
-
adapterName:
|
|
382
|
+
adapterName: getPlugin().name,
|
|
357
383
|
createMigrator: () => {
|
|
358
384
|
throw new Error(
|
|
359
385
|
"createMigrator is only available for Kysely/Prisma/Drizzle database adapters.",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { bench, describe } from "vitest";
|
|
2
|
+
|
|
2
3
|
import type {
|
|
3
4
|
DatabaseBundleQueryOptions,
|
|
4
5
|
DatabaseBundleQueryWhere,
|
|
@@ -230,7 +231,10 @@ describe("plugin update check benchmark", () => {
|
|
|
230
231
|
};
|
|
231
232
|
|
|
232
233
|
const plugin = createBenchPlugin(bundles);
|
|
233
|
-
const currentApi = createPluginDatabaseCore(
|
|
234
|
+
const currentApi = createPluginDatabaseCore(
|
|
235
|
+
() => plugin,
|
|
236
|
+
async () => null,
|
|
237
|
+
).api;
|
|
234
238
|
|
|
235
239
|
bench(
|
|
236
240
|
"pluginCore legacy full fetch",
|
package/src/db/types.ts
CHANGED
|
@@ -11,7 +11,8 @@ import type {
|
|
|
11
11
|
StoragePlugin,
|
|
12
12
|
} from "@hot-updater/plugin-core";
|
|
13
13
|
import type { FumaDBAdapter } from "fumadb/adapters";
|
|
14
|
-
|
|
14
|
+
|
|
15
|
+
import type { PaginatedResult } from "../types";
|
|
15
16
|
|
|
16
17
|
export type DatabasePluginFactory<TContext = unknown> =
|
|
17
18
|
() => DatabasePlugin<TContext>;
|
|
@@ -62,7 +63,7 @@ export interface DatabaseAPI<TContext = unknown> {
|
|
|
62
63
|
getBundles(
|
|
63
64
|
options: DatabaseBundleQueryOptions,
|
|
64
65
|
context?: HotUpdaterContext<TContext>,
|
|
65
|
-
): Promise<
|
|
66
|
+
): Promise<PaginatedResult>;
|
|
66
67
|
insertBundle(
|
|
67
68
|
bundle: Bundle,
|
|
68
69
|
context?: HotUpdaterContext<TContext>,
|
|
@@ -2,13 +2,14 @@ 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
4
|
import { createBlobDatabasePlugin } from "@hot-updater/plugin-core";
|
|
5
|
-
import { standaloneRepository } from "@hot-updater/standalone";
|
|
6
5
|
import { Kysely } from "kysely";
|
|
7
6
|
import { PGliteDialect } from "kysely-pglite-dialect";
|
|
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
|
+
|
|
12
|
+
import { standaloneRepository } from "../../../plugins/standalone/src";
|
|
12
13
|
import { kyselyAdapter } from "./adapters/kysely";
|
|
13
14
|
import { createHotUpdater } from "./db";
|
|
14
15
|
|
|
@@ -66,6 +67,9 @@ beforeAll(async () => {
|
|
|
66
67
|
http.get(`${baseUrl}/hot-updater/api/bundles`, ({ request }) =>
|
|
67
68
|
handleRequest(request),
|
|
68
69
|
),
|
|
70
|
+
http.get(`${baseUrl}/hot-updater/api/bundles/channels`, ({ request }) =>
|
|
71
|
+
handleRequest(request),
|
|
72
|
+
),
|
|
69
73
|
http.get(`${baseUrl}/hot-updater/api/bundles/:id`, ({ request }) =>
|
|
70
74
|
handleRequest(request),
|
|
71
75
|
),
|
|
@@ -245,6 +249,26 @@ describe("Handler <-> Standalone Repository Integration", () => {
|
|
|
245
249
|
expect(prodResult.data).toHaveLength(2);
|
|
246
250
|
});
|
|
247
251
|
|
|
252
|
+
it("Real integration: getChannels → handler GET /bundles/channels", async () => {
|
|
253
|
+
await api.insertBundle(
|
|
254
|
+
createTestBundle({ id: uuidv7(), channel: "production" }),
|
|
255
|
+
);
|
|
256
|
+
await api.insertBundle(createTestBundle({ id: uuidv7(), channel: "beta" }));
|
|
257
|
+
await api.insertBundle(
|
|
258
|
+
createTestBundle({ id: uuidv7(), channel: "production" }),
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
const repo = standaloneRepository({
|
|
262
|
+
baseUrl: `${baseUrl}/hot-updater`,
|
|
263
|
+
})();
|
|
264
|
+
|
|
265
|
+
const channels = await repo.getChannels();
|
|
266
|
+
|
|
267
|
+
expect(channels).toHaveLength(2);
|
|
268
|
+
expect(channels).toContain("production");
|
|
269
|
+
expect(channels).toContain("beta");
|
|
270
|
+
});
|
|
271
|
+
|
|
248
272
|
it("Full E2E: create → retrieve → update → delete via standalone", async () => {
|
|
249
273
|
const repo = standaloneRepository({
|
|
250
274
|
baseUrl: `${baseUrl}/hot-updater`,
|
package/src/handler.spec.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { NIL_UUID } from "@hot-updater/core";
|
|
1
|
+
import { type Bundle, NIL_UUID } from "@hot-updater/core";
|
|
2
2
|
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
3
4
|
import { createHandler, type HandlerAPI } from "./handler";
|
|
4
5
|
|
|
5
6
|
type TestEnv = {
|
|
@@ -10,22 +11,41 @@ type TestContext = {
|
|
|
10
11
|
env: TestEnv;
|
|
11
12
|
};
|
|
12
13
|
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
14
|
+
const testBundle: Bundle = {
|
|
15
|
+
id: "bundle-1",
|
|
16
|
+
platform: "ios",
|
|
17
|
+
shouldForceUpdate: false,
|
|
18
|
+
enabled: true,
|
|
19
|
+
fileHash: "hash123",
|
|
20
|
+
gitCommitHash: null,
|
|
21
|
+
message: "Test bundle",
|
|
22
|
+
channel: "production",
|
|
23
|
+
storageUri: "s3://test-bucket/bundles/bundle-1.zip",
|
|
24
|
+
targetAppVersion: "1.0.0",
|
|
25
|
+
fingerprintHash: null,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const createApi = () =>
|
|
29
|
+
({
|
|
30
|
+
getAppUpdateInfo: vi
|
|
31
|
+
.fn<HandlerAPI<TestContext>["getAppUpdateInfo"]>()
|
|
32
|
+
.mockResolvedValue({
|
|
33
|
+
fileHash: null,
|
|
34
|
+
fileUrl: null,
|
|
35
|
+
id: NIL_UUID,
|
|
36
|
+
message: null,
|
|
37
|
+
shouldForceUpdate: true,
|
|
38
|
+
status: "ROLLBACK",
|
|
39
|
+
}),
|
|
40
|
+
getBundleById: vi.fn<HandlerAPI<TestContext>["getBundleById"]>(),
|
|
41
|
+
getBundles: vi.fn<HandlerAPI<TestContext>["getBundles"]>(),
|
|
42
|
+
getChannels: vi
|
|
43
|
+
.fn<HandlerAPI<TestContext>["getChannels"]>()
|
|
44
|
+
.mockResolvedValue(["production"]),
|
|
45
|
+
insertBundle: vi.fn<HandlerAPI<TestContext>["insertBundle"]>(),
|
|
46
|
+
updateBundleById: vi.fn<HandlerAPI<TestContext>["updateBundleById"]>(),
|
|
47
|
+
deleteBundleById: vi.fn<HandlerAPI<TestContext>["deleteBundleById"]>(),
|
|
48
|
+
}) satisfies HandlerAPI<TestContext>;
|
|
29
49
|
|
|
30
50
|
describe("createHandler", () => {
|
|
31
51
|
it("supports the app-version route without a cohort segment", async () => {
|
|
@@ -134,9 +154,60 @@ describe("createHandler", () => {
|
|
|
134
154
|
);
|
|
135
155
|
|
|
136
156
|
expect(channelsResponse.status).toBe(200);
|
|
157
|
+
await expect(channelsResponse.json()).resolves.toEqual({
|
|
158
|
+
data: {
|
|
159
|
+
channels: ["production"],
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
expect(api.getChannels).toHaveBeenCalledWith(undefined);
|
|
137
163
|
expect(updateResponse.status).toBe(404);
|
|
138
164
|
});
|
|
139
165
|
|
|
166
|
+
it("returns paginated bundle results in the response body", async () => {
|
|
167
|
+
const api = createApi();
|
|
168
|
+
api.getBundles.mockResolvedValue({
|
|
169
|
+
data: [testBundle],
|
|
170
|
+
pagination: {
|
|
171
|
+
total: 51,
|
|
172
|
+
hasNextPage: true,
|
|
173
|
+
hasPreviousPage: true,
|
|
174
|
+
currentPage: 6,
|
|
175
|
+
totalPages: 26,
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
const handler = createHandler(api, { basePath: "/hot-updater" });
|
|
179
|
+
|
|
180
|
+
const response = await handler(
|
|
181
|
+
new Request(
|
|
182
|
+
"http://localhost/hot-updater/api/bundles?channel=production&platform=ios&limit=2&offset=10",
|
|
183
|
+
),
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
expect(response.status).toBe(200);
|
|
187
|
+
expect(response.headers.get("X-Total-Count")).toBeNull();
|
|
188
|
+
await expect(response.json()).resolves.toEqual({
|
|
189
|
+
data: [testBundle],
|
|
190
|
+
pagination: {
|
|
191
|
+
total: 51,
|
|
192
|
+
hasNextPage: true,
|
|
193
|
+
hasPreviousPage: true,
|
|
194
|
+
currentPage: 6,
|
|
195
|
+
totalPages: 26,
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
expect(api.getBundles).toHaveBeenCalledWith(
|
|
199
|
+
{
|
|
200
|
+
where: {
|
|
201
|
+
channel: "production",
|
|
202
|
+
platform: "ios",
|
|
203
|
+
},
|
|
204
|
+
limit: 2,
|
|
205
|
+
offset: 10,
|
|
206
|
+
},
|
|
207
|
+
undefined,
|
|
208
|
+
);
|
|
209
|
+
});
|
|
210
|
+
|
|
140
211
|
it("returns 400 when the platform route parameter is invalid", async () => {
|
|
141
212
|
const api = createApi();
|
|
142
213
|
const handler = createHandler(api, { basePath: "/hot-updater" });
|
package/src/handler.ts
CHANGED
|
@@ -9,8 +9,9 @@ import type {
|
|
|
9
9
|
DatabaseBundleQueryOptions,
|
|
10
10
|
HotUpdaterContext,
|
|
11
11
|
} from "@hot-updater/plugin-core";
|
|
12
|
+
|
|
12
13
|
import { addRoute, createRouter, findRoute } from "./internalRouter";
|
|
13
|
-
import type {
|
|
14
|
+
import type { ChannelsResponse, PaginatedResult } from "./types";
|
|
14
15
|
|
|
15
16
|
declare const __VERSION__: string;
|
|
16
17
|
|
|
@@ -27,7 +28,7 @@ export interface HandlerAPI<TContext = unknown> {
|
|
|
27
28
|
getBundles: (
|
|
28
29
|
options: DatabaseBundleQueryOptions,
|
|
29
30
|
context?: HotUpdaterContext<TContext>,
|
|
30
|
-
) => Promise<
|
|
31
|
+
) => Promise<PaginatedResult>;
|
|
31
32
|
insertBundle: (
|
|
32
33
|
bundle: Bundle,
|
|
33
34
|
context?: HotUpdaterContext<TContext>,
|
|
@@ -262,7 +263,7 @@ const handleGetBundles: RouteHandler = async (
|
|
|
262
263
|
context,
|
|
263
264
|
);
|
|
264
265
|
|
|
265
|
-
return new Response(JSON.stringify(result
|
|
266
|
+
return new Response(JSON.stringify(result), {
|
|
266
267
|
status: 200,
|
|
267
268
|
headers: { "Content-Type": "application/json" },
|
|
268
269
|
});
|
|
@@ -327,8 +328,13 @@ const handleGetChannels: RouteHandler = async (
|
|
|
327
328
|
context,
|
|
328
329
|
) => {
|
|
329
330
|
const channels = await api.getChannels(context);
|
|
331
|
+
const response: ChannelsResponse = {
|
|
332
|
+
data: {
|
|
333
|
+
channels,
|
|
334
|
+
},
|
|
335
|
+
};
|
|
330
336
|
|
|
331
|
-
return new Response(JSON.stringify(
|
|
337
|
+
return new Response(JSON.stringify(response), {
|
|
332
338
|
status: 200,
|
|
333
339
|
headers: { "Content-Type": "application/json" },
|
|
334
340
|
});
|
package/src/runtime.spec.ts
CHANGED
|
@@ -5,7 +5,9 @@ import type {
|
|
|
5
5
|
RequestEnvContext,
|
|
6
6
|
StoragePlugin,
|
|
7
7
|
} from "@hot-updater/plugin-core";
|
|
8
|
+
import { createDatabasePlugin } from "@hot-updater/plugin-core";
|
|
8
9
|
import { describe, expect, expectTypeOf, it, vi } from "vitest";
|
|
10
|
+
|
|
9
11
|
import { createHotUpdater } from "./runtime";
|
|
10
12
|
|
|
11
13
|
const bundle: Bundle = {
|
|
@@ -274,4 +276,157 @@ describe("runtime createHotUpdater", () => {
|
|
|
274
276
|
expect(response.status).toBe(200);
|
|
275
277
|
expect(getBundles).toHaveBeenCalledWith(expect.any(Object), undefined);
|
|
276
278
|
});
|
|
279
|
+
|
|
280
|
+
it("clears pending plugin changes after a failed mutation commit", async () => {
|
|
281
|
+
const committedBundles = new Map<string, Bundle>();
|
|
282
|
+
let commitAttempt = 0;
|
|
283
|
+
|
|
284
|
+
const database = createDatabasePlugin({
|
|
285
|
+
name: "failingPlugin",
|
|
286
|
+
factory: () => ({
|
|
287
|
+
async getBundleById(bundleId) {
|
|
288
|
+
return committedBundles.get(bundleId) ?? null;
|
|
289
|
+
},
|
|
290
|
+
async getBundles() {
|
|
291
|
+
return {
|
|
292
|
+
data: Array.from(committedBundles.values()),
|
|
293
|
+
pagination: {
|
|
294
|
+
hasNextPage: false,
|
|
295
|
+
hasPreviousPage: false,
|
|
296
|
+
currentPage: 1,
|
|
297
|
+
totalPages: 1,
|
|
298
|
+
total: committedBundles.size,
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
},
|
|
302
|
+
async getChannels() {
|
|
303
|
+
return [];
|
|
304
|
+
},
|
|
305
|
+
async commitBundle({ changedSets }) {
|
|
306
|
+
commitAttempt += 1;
|
|
307
|
+
|
|
308
|
+
if (commitAttempt === 1) {
|
|
309
|
+
throw new Error("commit failed");
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
for (const change of changedSets) {
|
|
313
|
+
if (change.operation === "delete") {
|
|
314
|
+
committedBundles.delete(change.data.id);
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
committedBundles.set(change.data.id, change.data);
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
}),
|
|
322
|
+
})({});
|
|
323
|
+
|
|
324
|
+
const hotUpdater = createHotUpdater({
|
|
325
|
+
database,
|
|
326
|
+
basePath: "/api/check-update",
|
|
327
|
+
routes: {
|
|
328
|
+
updateCheck: false,
|
|
329
|
+
bundles: false,
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
const failedBundle: Bundle = {
|
|
334
|
+
...bundle,
|
|
335
|
+
id: "00000000-0000-0000-0000-000000000010",
|
|
336
|
+
message: "failed bundle",
|
|
337
|
+
};
|
|
338
|
+
const succeedingBundle: Bundle = {
|
|
339
|
+
...bundle,
|
|
340
|
+
id: "00000000-0000-0000-0000-000000000011",
|
|
341
|
+
message: "succeeding bundle",
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
await expect(hotUpdater.insertBundle(failedBundle)).rejects.toThrow(
|
|
345
|
+
"commit failed",
|
|
346
|
+
);
|
|
347
|
+
await hotUpdater.insertBundle(succeedingBundle);
|
|
348
|
+
|
|
349
|
+
expect(await hotUpdater.getBundleById(failedBundle.id)).toBeNull();
|
|
350
|
+
await expect(
|
|
351
|
+
hotUpdater.getBundleById(succeedingBundle.id),
|
|
352
|
+
).resolves.toEqual(succeedingBundle);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it("isolates pending mutation state between overlapping writes", async () => {
|
|
356
|
+
const committedBundleIds: string[][] = [];
|
|
357
|
+
const onUnmount = vi.fn(async () => undefined);
|
|
358
|
+
let releaseFirstCommit!: () => void;
|
|
359
|
+
let notifyFirstCommitStarted!: () => void;
|
|
360
|
+
const firstCommitStarted = new Promise<void>((resolve) => {
|
|
361
|
+
notifyFirstCommitStarted = resolve;
|
|
362
|
+
});
|
|
363
|
+
const firstCommitGate = new Promise<void>((resolve) => {
|
|
364
|
+
releaseFirstCommit = resolve;
|
|
365
|
+
});
|
|
366
|
+
let commitCount = 0;
|
|
367
|
+
|
|
368
|
+
const database = createDatabasePlugin({
|
|
369
|
+
name: "isolatedPlugin",
|
|
370
|
+
factory: () => ({
|
|
371
|
+
async getBundleById() {
|
|
372
|
+
return null;
|
|
373
|
+
},
|
|
374
|
+
async getBundles() {
|
|
375
|
+
return {
|
|
376
|
+
data: [],
|
|
377
|
+
pagination: {
|
|
378
|
+
hasNextPage: false,
|
|
379
|
+
hasPreviousPage: false,
|
|
380
|
+
currentPage: 1,
|
|
381
|
+
totalPages: 1,
|
|
382
|
+
total: 0,
|
|
383
|
+
},
|
|
384
|
+
};
|
|
385
|
+
},
|
|
386
|
+
async getChannels() {
|
|
387
|
+
return [];
|
|
388
|
+
},
|
|
389
|
+
onUnmount,
|
|
390
|
+
async commitBundle({ changedSets }) {
|
|
391
|
+
commitCount += 1;
|
|
392
|
+
committedBundleIds.push(changedSets.map((change) => change.data.id));
|
|
393
|
+
|
|
394
|
+
if (commitCount === 1) {
|
|
395
|
+
notifyFirstCommitStarted();
|
|
396
|
+
await firstCommitGate;
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
}),
|
|
400
|
+
})({});
|
|
401
|
+
|
|
402
|
+
const hotUpdater = createHotUpdater({
|
|
403
|
+
database,
|
|
404
|
+
basePath: "/api/check-update",
|
|
405
|
+
routes: {
|
|
406
|
+
updateCheck: false,
|
|
407
|
+
bundles: false,
|
|
408
|
+
},
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
const firstBundleId = "00000000-0000-0000-0000-000000000020";
|
|
412
|
+
const secondBundleId = "00000000-0000-0000-0000-000000000021";
|
|
413
|
+
|
|
414
|
+
const firstInsert = hotUpdater.insertBundle({
|
|
415
|
+
...bundle,
|
|
416
|
+
id: firstBundleId,
|
|
417
|
+
message: "first bundle",
|
|
418
|
+
});
|
|
419
|
+
await firstCommitStarted;
|
|
420
|
+
|
|
421
|
+
const secondInsert = hotUpdater.insertBundle({
|
|
422
|
+
...bundle,
|
|
423
|
+
id: secondBundleId,
|
|
424
|
+
message: "second bundle",
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
releaseFirstCommit();
|
|
428
|
+
await Promise.all([firstInsert, secondInsert]);
|
|
429
|
+
|
|
430
|
+
expect(committedBundleIds).toEqual([[firstBundleId], [secondBundleId]]);
|
|
431
|
+
});
|
|
277
432
|
});
|
package/src/runtime.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
HotUpdaterContext,
|
|
3
3
|
StoragePlugin,
|
|
4
4
|
} from "@hot-updater/plugin-core";
|
|
5
|
+
|
|
5
6
|
import { createPluginDatabaseCore } from "./db/pluginCore";
|
|
6
7
|
import {
|
|
7
8
|
type DatabaseAdapter,
|
|
@@ -34,6 +35,7 @@ export interface CreateHotUpdaterOptions<TContext = unknown> {
|
|
|
34
35
|
export function createHotUpdater<TContext = unknown>(
|
|
35
36
|
options: CreateHotUpdaterOptions<TContext>,
|
|
36
37
|
): HotUpdaterAPI<TContext> {
|
|
38
|
+
const database = options.database;
|
|
37
39
|
const basePath = normalizeBasePath(options.basePath ?? "/api");
|
|
38
40
|
const storagePlugins = (options.storages ?? options.storagePlugins ?? []).map(
|
|
39
41
|
(plugin) => (typeof plugin === "function" ? plugin() : plugin),
|
|
@@ -70,21 +72,21 @@ export function createHotUpdater<TContext = unknown>(
|
|
|
70
72
|
return fileUrl;
|
|
71
73
|
};
|
|
72
74
|
|
|
73
|
-
if (
|
|
74
|
-
!isDatabasePluginFactory(options.database) &&
|
|
75
|
-
!isDatabasePlugin(options.database)
|
|
76
|
-
) {
|
|
75
|
+
if (!isDatabasePluginFactory(database) && !isDatabasePlugin(database)) {
|
|
77
76
|
throw new Error(
|
|
78
77
|
"@hot-updater/server/runtime only supports database plugins.",
|
|
79
78
|
);
|
|
80
79
|
}
|
|
81
80
|
|
|
82
|
-
const plugin = isDatabasePluginFactory(
|
|
83
|
-
? options.database()
|
|
84
|
-
: options.database;
|
|
81
|
+
const plugin = isDatabasePluginFactory(database) ? database() : database;
|
|
85
82
|
const core = createPluginDatabaseCore<TContext>(
|
|
86
|
-
plugin,
|
|
83
|
+
() => plugin,
|
|
87
84
|
resolveStoragePluginUrl,
|
|
85
|
+
isDatabasePluginFactory(database)
|
|
86
|
+
? {
|
|
87
|
+
createMutationPlugin: () => database(),
|
|
88
|
+
}
|
|
89
|
+
: undefined,
|
|
88
90
|
);
|
|
89
91
|
|
|
90
92
|
const api = {
|
package/src/types/index.ts
CHANGED
|
@@ -16,7 +16,16 @@ export interface PaginationOptions {
|
|
|
16
16
|
offset: number;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export interface
|
|
20
|
-
data:
|
|
19
|
+
export interface DataResponse<TData> {
|
|
20
|
+
data: TData;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface Paginated<TData> extends DataResponse<TData> {
|
|
21
24
|
pagination: PaginationInfo;
|
|
22
25
|
}
|
|
26
|
+
|
|
27
|
+
export type PaginatedResult = Paginated<Bundle[]>;
|
|
28
|
+
|
|
29
|
+
export type ChannelsResponse = DataResponse<{
|
|
30
|
+
channels: string[];
|
|
31
|
+
}>;
|