@hot-updater/server 0.36.7 → 1.0.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/databaseConstraintErrors.cjs +23 -0
- package/dist/adapters/databaseConstraintErrors.mjs +21 -0
- package/dist/adapters/databasePluginUtils.cjs +117 -0
- package/dist/adapters/databasePluginUtils.mjs +106 -0
- package/dist/adapters/drizzle.cjs +53 -176
- package/dist/adapters/drizzle.d.cts +6 -4
- package/dist/adapters/drizzle.d.mts +6 -4
- package/dist/adapters/drizzle.mjs +54 -177
- package/dist/adapters/drizzleCrud.cjs +250 -0
- package/dist/adapters/drizzleCrud.mjs +249 -0
- package/dist/adapters/drizzleLazyDB.cjs +84 -36
- package/dist/adapters/drizzleLazyDB.mjs +84 -36
- package/dist/adapters/drizzleQuery.cjs +68 -0
- package/dist/adapters/drizzleQuery.mjs +67 -0
- package/dist/adapters/kysely.cjs +18 -138
- package/dist/adapters/kysely.d.cts +3 -12
- package/dist/adapters/kysely.d.mts +3 -12
- package/dist/adapters/kysely.mjs +19 -139
- package/dist/adapters/kyselyCrud.cjs +254 -0
- package/dist/adapters/kyselyCrud.mjs +253 -0
- package/dist/adapters/kyselyQuery.cjs +56 -0
- package/dist/adapters/kyselyQuery.mjs +55 -0
- package/dist/adapters/mongodb.cjs +21 -174
- package/dist/adapters/mongodb.d.cts +3 -2
- package/dist/adapters/mongodb.d.mts +3 -2
- package/dist/adapters/mongodb.mjs +21 -174
- package/dist/adapters/mongodbCollections.cjs +36 -0
- package/dist/adapters/mongodbCollections.mjs +30 -0
- package/dist/adapters/mongodbQuery.cjs +77 -0
- package/dist/adapters/mongodbQuery.mjs +70 -0
- package/dist/adapters/mongodbReads.cjs +148 -0
- package/dist/adapters/mongodbReads.mjs +147 -0
- package/dist/adapters/mongodbWrites.cjs +134 -0
- package/dist/adapters/mongodbWrites.mjs +133 -0
- package/dist/adapters/prisma.cjs +205 -180
- package/dist/adapters/prisma.d.cts +3 -3
- package/dist/adapters/prisma.d.mts +3 -3
- package/dist/adapters/prisma.mjs +206 -181
- package/dist/adapters/prismaQuery.cjs +58 -0
- package/dist/adapters/prismaQuery.mjs +56 -0
- package/dist/adapters/prismaRows.cjs +198 -0
- package/dist/adapters/prismaRows.mjs +188 -0
- package/dist/analytics/bounded/activeOverview.cjs +87 -0
- package/dist/analytics/bounded/activeOverview.mjs +85 -0
- package/dist/analytics/bounded/installationSearch.cjs +42 -0
- package/dist/analytics/bounded/installationSearch.mjs +42 -0
- package/dist/analytics/bounded/persistence.cjs +39 -0
- package/dist/analytics/bounded/persistence.mjs +38 -0
- package/dist/analytics/bounded/provider.cjs +170 -0
- package/dist/analytics/bounded/provider.d.cts +7 -0
- package/dist/analytics/bounded/provider.d.mts +7 -0
- package/dist/analytics/bounded/provider.mjs +170 -0
- package/dist/analytics/bounded/scan.cjs +144 -0
- package/dist/analytics/bounded/scan.mjs +138 -0
- package/dist/analytics/domain.d.cts +121 -0
- package/dist/analytics/domain.d.mts +121 -0
- package/dist/analytics/errors.cjs +22 -0
- package/dist/analytics/errors.mjs +20 -0
- package/dist/analytics/eventInput.cjs +111 -0
- package/dist/analytics/eventInput.mjs +111 -0
- package/dist/analytics/persistence.d.cts +6 -0
- package/dist/analytics/persistence.d.mts +6 -0
- package/dist/analytics/queryInput.cjs +58 -0
- package/dist/analytics/queryInput.mjs +55 -0
- package/dist/analytics/routes.cjs +63 -0
- package/dist/analytics/routes.mjs +61 -0
- package/dist/analytics/types.d.cts +17 -0
- package/dist/analytics/types.d.mts +17 -0
- package/dist/apiKeys.cjs +139 -0
- package/dist/apiKeys.d.cts +50 -0
- package/dist/apiKeys.d.mts +50 -0
- package/dist/apiKeys.mjs +133 -0
- package/dist/createHotUpdaterCore.cjs +48 -26
- package/dist/createHotUpdaterCore.d.cts +49 -17
- package/dist/createHotUpdaterCore.d.mts +49 -17
- package/dist/createHotUpdaterCore.mjs +51 -29
- package/dist/db/createBundleDiff.cjs +45 -68
- package/dist/db/createBundleDiff.d.cts +3 -3
- package/dist/db/createBundleDiff.d.mts +3 -3
- package/dist/db/createBundleDiff.mjs +47 -68
- package/dist/db/databasePluginCore.cjs +138 -0
- package/dist/db/databasePluginCore.mjs +137 -0
- package/dist/db/fixedMigrator.cjs +36 -93
- package/dist/db/fixedMigrator.mjs +36 -93
- package/dist/db/fixedMigratorMongo.cjs +319 -0
- package/dist/db/fixedMigratorMongo.mjs +319 -0
- package/dist/db/fixedMigratorShared.cjs +23 -0
- package/dist/db/fixedMigratorShared.mjs +18 -0
- package/dist/db/mongoMigrationExecution.cjs +11 -0
- package/dist/db/mongoMigrationExecution.mjs +11 -0
- package/dist/db/releaseCatalog.cjs +66 -0
- package/dist/db/releaseCatalog.d.cts +17 -0
- package/dist/db/releaseCatalog.d.mts +17 -0
- package/dist/db/releaseCatalog.mjs +64 -0
- package/dist/db/schema/mongodb.cjs +14 -6
- package/dist/db/schema/mongodb.mjs +14 -6
- package/dist/db/schema/registry.cjs +0 -2
- package/dist/db/schema/registry.mjs +1 -2
- package/dist/db/schema/sql.cjs +31 -14
- package/dist/db/schema/sql.mjs +32 -13
- package/dist/db/schema/sqlOperations.cjs +4 -4
- package/dist/db/schema/sqlOperations.mjs +5 -5
- package/dist/db/schemaGenerators.cjs +52 -18
- package/dist/db/schemaGenerators.mjs +52 -18
- package/dist/db/schemaReadiness.cjs +4 -3
- package/dist/db/schemaReadiness.mjs +4 -3
- package/dist/db/sqlMigrationExecution.cjs +67 -0
- package/dist/db/sqlMigrationExecution.mjs +66 -0
- package/dist/db/types.cjs +2 -6
- package/dist/db/types.d.cts +60 -16
- package/dist/db/types.d.mts +60 -16
- package/dist/db/types.mjs +3 -6
- package/dist/db/updateArtifacts.cjs +114 -60
- package/dist/db/updateArtifacts.mjs +117 -62
- package/dist/handler.cjs +108 -282
- package/dist/handler.d.cts +3 -46
- package/dist/handler.d.mts +3 -46
- package/dist/handler.mjs +107 -281
- package/dist/handlerBundleRoutes.cjs +155 -0
- package/dist/handlerBundleRoutes.mjs +155 -0
- package/dist/handlerErrors.cjs +9 -0
- package/dist/handlerErrors.mjs +9 -0
- package/dist/handlerParameters.cjs +39 -0
- package/dist/handlerParameters.mjs +34 -0
- package/dist/handlerReleaseCatalogRoutes.cjs +111 -0
- package/dist/handlerReleaseCatalogRoutes.mjs +110 -0
- package/dist/handlerReleaseManagementRoutes.cjs +125 -0
- package/dist/handlerReleaseManagementRoutes.mjs +124 -0
- package/dist/handlerTypes.d.cts +63 -0
- package/dist/handlerTypes.d.mts +63 -0
- package/dist/handlerVersionRoutes.cjs +10 -0
- package/dist/handlerVersionRoutes.d.cts +4 -0
- package/dist/handlerVersionRoutes.d.mts +4 -0
- package/dist/handlerVersionRoutes.mjs +9 -0
- package/dist/index.cjs +10 -1
- package/dist/index.d.cts +9 -3
- package/dist/index.d.mts +9 -3
- package/dist/index.mjs +5 -2
- package/dist/node.cjs +19 -12
- package/dist/node.d.cts +15 -10
- package/dist/node.d.mts +15 -10
- package/dist/node.mjs +19 -13
- package/dist/package.cjs +1 -1
- package/dist/package.mjs +1 -1
- package/dist/schema/dsl.cjs +19 -2
- package/dist/schema/dsl.mjs +17 -3
- package/dist/schema/index.cjs +1 -8
- package/dist/schema/index.mjs +2 -8
- package/dist/schema/types.cjs +3 -1
- package/dist/schema/types.mjs +3 -2
- package/dist/schema/v1_0_0.cjs +364 -0
- package/dist/schema/v1_0_0.mjs +357 -0
- package/dist/storageAccess.cjs +69 -25
- package/dist/storageAccess.mjs +68 -25
- package/dist/types/index.d.cts +4 -3
- package/dist/types/index.d.mts +5 -4
- package/package.json +8 -7
- package/src/adapters/databaseConstraintErrors.ts +25 -0
- package/src/adapters/databasePluginTestDatabase.ts +106 -0
- package/src/adapters/databasePluginUtils.spec.ts +29 -0
- package/src/adapters/databasePluginUtils.ts +214 -0
- package/src/adapters/drizzle.spec.ts +208 -47
- package/src/adapters/drizzle.ts +106 -293
- package/src/adapters/drizzleCrud.ts +419 -0
- package/src/adapters/drizzleLazyDB.ts +206 -100
- package/src/adapters/drizzleQuery.ts +164 -0
- package/src/adapters/kysely.spec.ts +126 -88
- package/src/adapters/kysely.ts +76 -334
- package/src/adapters/kyselyCrud.ts +588 -0
- package/src/adapters/kyselyQuery.ts +128 -0
- package/src/adapters/mongodb.spec.ts +141 -0
- package/src/adapters/mongodb.ts +60 -288
- package/src/adapters/mongodbCollections.ts +62 -0
- package/src/adapters/mongodbQuery.ts +226 -0
- package/src/adapters/mongodbReads.ts +311 -0
- package/src/adapters/mongodbRows.spec.ts +64 -0
- package/src/adapters/mongodbRows.ts +118 -0
- package/src/adapters/mongodbTestClient.ts +342 -0
- package/src/adapters/mongodbTestFilter.ts +166 -0
- package/src/adapters/mongodbWrites.ts +260 -0
- package/src/adapters/prisma.spec.ts +196 -0
- package/src/adapters/prisma.ts +417 -292
- package/src/adapters/prismaQuery.ts +129 -0
- package/src/adapters/prismaRows.spec.ts +98 -0
- package/src/adapters/prismaRows.ts +301 -0
- package/src/adapters/prismaTestClient.ts +396 -0
- package/src/analytics/analytics.integration.spec.ts +144 -0
- package/src/analytics/bounded/activeOverview.spec.ts +189 -0
- package/src/analytics/bounded/activeOverview.ts +148 -0
- package/src/analytics/bounded/installationSearch.spec.ts +130 -0
- package/src/analytics/bounded/installationSearch.ts +69 -0
- package/src/analytics/bounded/persistence.ts +43 -0
- package/src/analytics/bounded/provider.spec.ts +219 -0
- package/src/analytics/bounded/provider.ts +229 -0
- package/src/analytics/bounded/scan.ts +235 -0
- package/src/analytics/domain.ts +139 -0
- package/src/analytics/errors.ts +18 -0
- package/src/analytics/eventInput.ts +168 -0
- package/src/analytics/persistence.ts +10 -0
- package/src/analytics/queryInput.ts +102 -0
- package/src/analytics/routes.ts +123 -0
- package/src/analytics/types.ts +44 -0
- package/src/apiKeys.integration.spec.ts +192 -0
- package/src/apiKeys.spec.ts +162 -0
- package/src/apiKeys.ts +220 -0
- package/src/createHotUpdaterCore.ts +164 -100
- package/src/db/bundleRows.spec.ts +164 -0
- package/src/db/bundleRows.ts +10 -140
- package/src/db/createBundleDiff.spec.ts +172 -127
- package/src/db/createBundleDiff.ts +91 -110
- package/src/db/databasePluginCore.spec.ts +62 -0
- package/src/db/databasePluginCore.testFixtures.ts +76 -0
- package/src/db/databasePluginCore.ts +185 -0
- package/src/db/databasePluginUpdateCheck.bench.ts +107 -0
- package/src/db/fixedMigrator.spec.ts +184 -42
- package/src/db/fixedMigrator.ts +64 -205
- package/src/db/fixedMigratorMongo.spec.ts +192 -0
- package/src/db/fixedMigratorMongo.ts +425 -0
- package/src/db/fixedMigratorShared.ts +36 -0
- package/src/db/hotUpdaterSchema.ts +0 -1
- package/src/db/index.spec.ts +333 -1025
- package/src/db/mongoMigrationExecution.ts +23 -0
- package/src/db/node-sqlite.d.ts +25 -0
- package/src/db/releaseCatalog.ts +148 -0
- package/src/db/schema/mongodb.ts +20 -6
- package/src/db/schema/sql.ts +52 -11
- package/src/db/schema/sqlOperations.ts +5 -4
- package/src/db/schemaGenerators.ts +107 -23
- package/src/db/schemaReadiness.ts +2 -1
- package/src/db/sqlMigrationExecution.spec.ts +65 -0
- package/src/db/sqlMigrationExecution.ts +108 -0
- package/src/db/types.spec.ts +55 -0
- package/src/db/types.ts +129 -65
- package/src/db/updateArtifacts.spec.ts +359 -0
- package/src/db/updateArtifacts.ts +290 -126
- package/src/handler-standalone.integration.spec.ts +401 -364
- package/src/handler.spec.ts +137 -582
- package/src/handler.testFixtures.ts +30 -0
- package/src/handler.ts +218 -616
- package/src/handlerAuth.integration.spec.ts +33 -0
- package/src/handlerBundleRoutes.ts +259 -0
- package/src/handlerErrors.ts +7 -0
- package/src/handlerManagement.spec.ts +262 -0
- package/src/handlerParameters.ts +105 -0
- package/src/handlerReleaseCatalogRoutes.ts +154 -0
- package/src/handlerReleaseManagementRoutes.ts +213 -0
- package/src/handlerTypes.ts +96 -0
- package/src/handlerVersionRoutes.ts +12 -0
- package/src/index.ts +22 -2
- package/src/node.spec.ts +79 -10
- package/src/node.ts +20 -17
- package/src/releaseCatalogRoutes.integration.spec.ts +238 -0
- package/src/runtime.spec.ts +126 -959
- package/src/runtime.testFixtures.ts +71 -0
- package/src/schema/dsl-all-versions.spec.ts +26 -17
- package/src/schema/dsl.ts +41 -2
- package/src/schema/index.ts +3 -9
- package/src/schema/types.ts +9 -3
- package/src/schema/v1_0_0.spec.ts +146 -0
- package/src/schema/v1_0_0.ts +465 -0
- package/src/storageAccess.spec.ts +127 -32
- package/src/storageAccess.ts +106 -55
- package/src/types/index.ts +2 -1
- package/dist/db/bundleRows.cjs +0 -82
- package/dist/db/bundleRows.d.cts +0 -33
- package/dist/db/bundleRows.d.mts +0 -33
- package/dist/db/bundleRows.mjs +0 -79
- package/dist/db/pluginCore.cjs +0 -242
- package/dist/db/pluginCore.mjs +0 -241
- package/dist/db/schema/sqlMigrations.cjs +0 -74
- package/dist/db/schema/sqlMigrations.mjs +0 -73
- package/dist/db/schemaEnhancements.cjs +0 -20
- package/dist/db/schemaEnhancements.mjs +0 -19
- package/dist/route.cjs +0 -7
- package/dist/route.mjs +0 -7
- package/dist/schema/v0_21_0.cjs +0 -38
- package/dist/schema/v0_21_0.mjs +0 -37
- package/dist/schema/v0_29_0.cjs +0 -45
- package/dist/schema/v0_29_0.mjs +0 -44
- package/dist/schema/v0_31_0.cjs +0 -82
- package/dist/schema/v0_31_0.mjs +0 -80
- package/src/db/pluginCore.spec.ts +0 -1095
- package/src/db/pluginCore.ts +0 -475
- package/src/db/pluginUpdateCheck.bench.ts +0 -254
- package/src/db/schema/sqlMigrations.ts +0 -219
- package/src/db/schemaEnhancements.ts +0 -55
- package/src/route.ts +0 -7
- package/src/schema/v0_21_0.ts +0 -52
- package/src/schema/v0_29_0.ts +0 -62
- package/src/schema/v0_31_0.spec.ts +0 -73
- package/src/schema/v0_31_0.ts +0 -120
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import { PGlite } from "@electric-sql/pglite";
|
|
2
2
|
import type { Bundle } from "@hot-updater/core";
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
createReleaseCatalogScopeKey,
|
|
5
|
+
encodeChannelKey,
|
|
6
|
+
NIL_UUID,
|
|
7
|
+
} from "@hot-updater/core";
|
|
8
|
+
import {
|
|
9
|
+
bundleToRow,
|
|
10
|
+
commitReleaseCatalogMutation,
|
|
11
|
+
createDatabaseClient,
|
|
12
|
+
} from "@hot-updater/plugin-core";
|
|
5
13
|
import { Kysely } from "kysely";
|
|
6
14
|
import { PGliteDialect } from "kysely-pglite-dialect";
|
|
7
15
|
import { HttpResponse, http } from "msw";
|
|
@@ -10,90 +18,60 @@ import { uuidv7 } from "uuidv7";
|
|
|
10
18
|
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
11
19
|
|
|
12
20
|
import { standaloneRepository } from "../../../plugins/standalone/src";
|
|
21
|
+
import { createInMemoryDatabasePlugin } from "../../test-utils/test/inMemoryDatabasePlugin";
|
|
13
22
|
import { kyselyAdapter } from "./adapters/kysely";
|
|
14
23
|
import { createMigrator } from "./db";
|
|
15
|
-
import { createHotUpdater } from "./index";
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Integration tests between @hot-updater/server handler and @hot-updater/standalone repository
|
|
19
|
-
*
|
|
20
|
-
* This test suite verifies real-world compatibility by:
|
|
21
|
-
* 1. Using actual standaloneRepository (not mocks)
|
|
22
|
-
* 2. Using actual handler (not mocks)
|
|
23
|
-
* 3. Simulating HTTP communication via MSW
|
|
24
|
-
* 4. Testing end-to-end flows: standalone → HTTP → handler → database
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
// Create in-memory database for testing
|
|
24
|
+
import { createHotUpdater, type RuntimeHotUpdaterAPI } from "./index";
|
|
25
|
+
|
|
28
26
|
const db = new PGlite();
|
|
29
27
|
const kysely = new Kysely<object>({ dialect: new PGliteDialect(db) });
|
|
30
|
-
|
|
31
|
-
// Create handler API with in-memory DB
|
|
28
|
+
const clientMountPath = "/hot-updater";
|
|
32
29
|
const api = createHotUpdater({
|
|
33
|
-
database: kyselyAdapter({
|
|
34
|
-
|
|
35
|
-
provider: "postgresql",
|
|
36
|
-
}),
|
|
37
|
-
basePath: "/hot-updater",
|
|
38
|
-
routes: {
|
|
39
|
-
updateCheck: true,
|
|
40
|
-
bundles: true,
|
|
41
|
-
},
|
|
30
|
+
database: kyselyAdapter({ db: kysely, provider: "postgresql" }),
|
|
31
|
+
clientAccess: { type: "public" },
|
|
42
32
|
});
|
|
43
|
-
|
|
44
|
-
// Setup MSW server to intercept HTTP requests
|
|
45
33
|
const baseUrl = "http://localhost:3000";
|
|
46
34
|
const server = setupServer();
|
|
35
|
+
const invokeHandler = async (
|
|
36
|
+
hotUpdater: RuntimeHotUpdaterAPI,
|
|
37
|
+
request: Request,
|
|
38
|
+
clientPath: string,
|
|
39
|
+
) => {
|
|
40
|
+
const url = new URL(request.url);
|
|
41
|
+
const adminBasePath = `${clientPath}/admin`;
|
|
42
|
+
const isAdmin = url.pathname.startsWith(`${adminBasePath}/`);
|
|
43
|
+
const mountPath = isAdmin ? adminBasePath : clientPath;
|
|
44
|
+
url.pathname = url.pathname.slice(mountPath.length) || "/";
|
|
45
|
+
const handler = isAdmin
|
|
46
|
+
? hotUpdater.handlers.admin
|
|
47
|
+
: hotUpdater.handlers.client;
|
|
48
|
+
const response = await handler(new Request(url, request));
|
|
49
|
+
return new HttpResponse(await response.text(), {
|
|
50
|
+
status: response.status,
|
|
51
|
+
headers: response.headers,
|
|
52
|
+
});
|
|
53
|
+
};
|
|
47
54
|
|
|
48
55
|
beforeAll(async () => {
|
|
49
|
-
|
|
50
|
-
const migrator = createMigrator(api);
|
|
51
|
-
const result = await migrator.migrateToLatest({
|
|
56
|
+
const result = await createMigrator(api).migrateToLatest({
|
|
52
57
|
mode: "from-schema",
|
|
53
58
|
updateSettings: true,
|
|
54
59
|
});
|
|
55
60
|
await result.execute();
|
|
56
|
-
|
|
57
|
-
// Start MSW server
|
|
58
61
|
server.listen({ onUnhandledRequest: "error" });
|
|
59
|
-
|
|
60
|
-
// Route all requests to our handler
|
|
61
|
-
const handleRequest = async (request: Request) => {
|
|
62
|
-
const response = await api.handler(request);
|
|
63
|
-
const data = (await response.json()) as Record<string, unknown>;
|
|
64
|
-
return HttpResponse.json(data, {
|
|
65
|
-
status: response.status,
|
|
66
|
-
headers: response.headers,
|
|
67
|
-
});
|
|
68
|
-
};
|
|
69
|
-
|
|
70
62
|
server.use(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
handleRequest(request),
|
|
74
|
-
),
|
|
75
|
-
http.get(`${baseUrl}/hot-updater/api/bundles/channels`, ({ request }) =>
|
|
76
|
-
handleRequest(request),
|
|
77
|
-
),
|
|
78
|
-
http.get(`${baseUrl}/hot-updater/api/bundles/:id`, ({ request }) =>
|
|
79
|
-
handleRequest(request),
|
|
80
|
-
),
|
|
81
|
-
http.post(`${baseUrl}/hot-updater/api/bundles`, ({ request }) =>
|
|
82
|
-
handleRequest(request),
|
|
83
|
-
),
|
|
84
|
-
http.patch(`${baseUrl}/hot-updater/api/bundles/:id`, ({ request }) =>
|
|
85
|
-
handleRequest(request),
|
|
86
|
-
),
|
|
87
|
-
http.delete(`${baseUrl}/hot-updater/api/bundles/:id`, ({ request }) =>
|
|
88
|
-
handleRequest(request),
|
|
63
|
+
http.all(`${baseUrl}/hot-updater/*`, ({ request }) =>
|
|
64
|
+
invokeHandler(api, request, clientMountPath),
|
|
89
65
|
),
|
|
90
66
|
);
|
|
91
67
|
});
|
|
92
68
|
|
|
93
69
|
afterEach(async () => {
|
|
94
|
-
// Clean up database after each test
|
|
95
70
|
await db.exec("DELETE FROM bundle_patches");
|
|
71
|
+
await db.exec("DELETE FROM release_catalogs");
|
|
72
|
+
await db.exec("DELETE FROM releases");
|
|
96
73
|
await db.exec("DELETE FROM bundles");
|
|
74
|
+
await db.exec("DELETE FROM channels");
|
|
97
75
|
});
|
|
98
76
|
|
|
99
77
|
afterAll(async () => {
|
|
@@ -105,389 +83,448 @@ afterAll(async () => {
|
|
|
105
83
|
const createTestBundle = (overrides?: Partial<Bundle>): Bundle => ({
|
|
106
84
|
id: NIL_UUID,
|
|
107
85
|
platform: "ios",
|
|
108
|
-
channel: "production",
|
|
109
|
-
enabled: true,
|
|
110
|
-
shouldForceUpdate: false,
|
|
111
86
|
fileHash: "test-hash",
|
|
112
87
|
gitCommitHash: null,
|
|
113
|
-
message: null,
|
|
114
|
-
targetAppVersion: "*",
|
|
115
88
|
storageUri: "test://storage",
|
|
116
|
-
|
|
89
|
+
archiveByteSize: 3_000_000_001,
|
|
117
90
|
...overrides,
|
|
118
91
|
});
|
|
119
92
|
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
93
|
+
const createStandaloneClient = (base = `${baseUrl}/hot-updater/admin`) =>
|
|
94
|
+
createDatabaseClient(standaloneRepository({ baseUrl: base }));
|
|
95
|
+
|
|
96
|
+
const commitTestRelease = async (
|
|
97
|
+
bundle: Bundle,
|
|
98
|
+
channelName = "production",
|
|
99
|
+
) => {
|
|
100
|
+
const repository = standaloneRepository({
|
|
101
|
+
baseUrl: `${baseUrl}/hot-updater/admin`,
|
|
102
|
+
});
|
|
103
|
+
const channel = await repository.models.channels.insert({
|
|
104
|
+
onConflict: "returnExisting",
|
|
105
|
+
row: { id: uuidv7(), name: channelName },
|
|
106
|
+
});
|
|
107
|
+
const scopeKey = createReleaseCatalogScopeKey({
|
|
108
|
+
channelKey: encodeChannelKey(channelName),
|
|
109
|
+
platform: bundle.platform,
|
|
110
|
+
strategy: "APP_VERSION",
|
|
111
|
+
});
|
|
112
|
+
const releaseId = uuidv7();
|
|
113
|
+
const committed = await commitReleaseCatalogMutation({
|
|
114
|
+
companionChanges: [
|
|
115
|
+
{
|
|
116
|
+
model: "bundles",
|
|
117
|
+
operation: "insert",
|
|
118
|
+
row: bundleToRow(bundle),
|
|
133
119
|
},
|
|
134
|
-
|
|
135
|
-
|
|
120
|
+
],
|
|
121
|
+
database: repository,
|
|
122
|
+
mutation: {
|
|
123
|
+
operation: "insert",
|
|
124
|
+
row: {
|
|
125
|
+
bundle_id: bundle.id,
|
|
126
|
+
channel_id: channel.row.id,
|
|
127
|
+
created_at_ms: 1,
|
|
128
|
+
enabled: true,
|
|
129
|
+
fingerprint_hash: null,
|
|
130
|
+
id: releaseId,
|
|
131
|
+
kind: "BUNDLE",
|
|
132
|
+
message: null,
|
|
133
|
+
operation: "DEPLOY",
|
|
134
|
+
platform: bundle.platform,
|
|
135
|
+
revision: 1,
|
|
136
|
+
rollout_cohort_count: 1_000,
|
|
137
|
+
scope_key: scopeKey,
|
|
138
|
+
should_force_update: false,
|
|
139
|
+
source_release_id: null,
|
|
140
|
+
strategy: "APP_VERSION",
|
|
141
|
+
target_app_version: "*",
|
|
142
|
+
target_cohorts: [],
|
|
143
|
+
updated_at_ms: 1,
|
|
136
144
|
},
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
145
|
+
},
|
|
146
|
+
scope: {
|
|
147
|
+
channelId: channel.row.id,
|
|
148
|
+
channelName,
|
|
149
|
+
fingerprintHash: null,
|
|
150
|
+
platform: bundle.platform,
|
|
151
|
+
scopeKey,
|
|
152
|
+
strategy: "APP_VERSION",
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
return { channel: channel.row, committed, releaseId, repository, scopeKey };
|
|
156
|
+
};
|
|
140
157
|
|
|
141
158
|
describe("Handler <-> Standalone Repository Integration", () => {
|
|
142
|
-
it("
|
|
143
|
-
// Create standalone repository pointing to our test server
|
|
144
|
-
const repo = standaloneRepository({
|
|
145
|
-
baseUrl: `${baseUrl}/hot-updater`,
|
|
146
|
-
})();
|
|
147
|
-
|
|
159
|
+
it("uses Standalone for bundle management and the server database for Analytics", async () => {
|
|
148
160
|
const bundleId = uuidv7();
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
161
|
+
const installId = "standalone-install";
|
|
162
|
+
await createStandaloneClient().insertBundle(
|
|
163
|
+
createTestBundle({ id: bundleId }),
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const ingestion = await api.handlers.client(
|
|
167
|
+
new Request(`${baseUrl}/events`, {
|
|
168
|
+
method: "POST",
|
|
169
|
+
headers: {
|
|
170
|
+
"content-type": "application/json",
|
|
171
|
+
},
|
|
172
|
+
body: JSON.stringify({
|
|
173
|
+
appVersion: "1.0.0",
|
|
174
|
+
channel: "production",
|
|
175
|
+
cohort: "default",
|
|
176
|
+
fingerprintHash: null,
|
|
177
|
+
fromBundleId: NIL_UUID,
|
|
178
|
+
fromReleaseId: null,
|
|
179
|
+
installId,
|
|
180
|
+
platform: "ios",
|
|
181
|
+
sdkVersion: "2.0.0",
|
|
182
|
+
toBundleId: bundleId,
|
|
183
|
+
toReleaseId: null,
|
|
184
|
+
type: "UPDATE_APPLIED",
|
|
185
|
+
updateStrategy: "appVersion",
|
|
186
|
+
}),
|
|
187
|
+
}),
|
|
188
|
+
);
|
|
189
|
+
const overview = await api.handlers.admin(
|
|
190
|
+
new Request(`${baseUrl}/installations/overview`),
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
expect(ingestion.status).toBe(204);
|
|
194
|
+
expect(overview.status).toBe(200);
|
|
195
|
+
await expect(overview.json()).resolves.toEqual({
|
|
196
|
+
bundles: [{ bundleId, installations: 1 }],
|
|
197
|
+
trackedInstallations: 1,
|
|
152
198
|
});
|
|
199
|
+
});
|
|
153
200
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
201
|
+
it("atomically commits and reads Release catalogs through standalone management routes", async () => {
|
|
202
|
+
const bundle = createTestBundle({ id: uuidv7() });
|
|
203
|
+
const { committed, releaseId, repository, scopeKey } =
|
|
204
|
+
await commitTestRelease(bundle);
|
|
157
205
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
206
|
+
await expect(
|
|
207
|
+
repository.models.releases.findById(releaseId),
|
|
208
|
+
).resolves.toMatchObject({
|
|
209
|
+
bundle_id: bundle.id,
|
|
210
|
+
revision: 1,
|
|
211
|
+
});
|
|
212
|
+
await expect(
|
|
213
|
+
repository.models.releaseCatalogs.findByScopeKey(scopeKey),
|
|
214
|
+
).resolves.toMatchObject({ generation: committed.catalog.generation });
|
|
215
|
+
const response = await api.handlers.client(
|
|
216
|
+
new Request(
|
|
217
|
+
`${baseUrl}/release-catalogs/app-version/ios/${encodeChannelKey("production")}/1.0.0`,
|
|
218
|
+
),
|
|
164
219
|
);
|
|
165
|
-
|
|
166
|
-
const response = await api.handler(request);
|
|
167
220
|
expect(response.status).toBe(200);
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
expect(retrieved.fileHash).toBe("integration-hash-1");
|
|
221
|
+
await expect(response.json()).resolves.toMatchObject({
|
|
222
|
+
releases: [{ bundleId: bundle.id, releaseId }],
|
|
223
|
+
});
|
|
172
224
|
});
|
|
173
225
|
|
|
174
|
-
it("
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
id: bundleId,
|
|
179
|
-
fileHash: "get-hash-1",
|
|
226
|
+
it("keeps Standalone management routes separate from API key protection", async () => {
|
|
227
|
+
const protectedApi = createHotUpdater({
|
|
228
|
+
database: createInMemoryDatabasePlugin(),
|
|
229
|
+
clientAccess: { type: "api-key" },
|
|
180
230
|
});
|
|
231
|
+
server.use(
|
|
232
|
+
http.all(`${baseUrl}/protected-hot-updater/*`, async ({ request }) => {
|
|
233
|
+
return invokeHandler(protectedApi, request, "/protected-hot-updater");
|
|
234
|
+
}),
|
|
235
|
+
);
|
|
236
|
+
const bundleId = uuidv7();
|
|
237
|
+
const client = createStandaloneClient(
|
|
238
|
+
`${baseUrl}/protected-hot-updater/admin`,
|
|
239
|
+
);
|
|
181
240
|
|
|
182
|
-
await
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
// Use standalone repository to retrieve
|
|
190
|
-
const retrieved = await repo.getBundleById(bundleId);
|
|
241
|
+
await client.insertBundle(createTestBundle({ id: bundleId }));
|
|
242
|
+
const updateCheck = await protectedApi.handlers.client(
|
|
243
|
+
new Request(
|
|
244
|
+
`${baseUrl}/release-catalogs/app-version/ios/${encodeChannelKey("production")}/1.0.0`,
|
|
245
|
+
),
|
|
246
|
+
);
|
|
191
247
|
|
|
192
|
-
expect(
|
|
193
|
-
|
|
194
|
-
|
|
248
|
+
await expect(client.getBundleById(bundleId)).resolves.toMatchObject({
|
|
249
|
+
id: bundleId,
|
|
250
|
+
});
|
|
251
|
+
expect(updateCheck.status).toBe(401);
|
|
195
252
|
});
|
|
196
253
|
|
|
197
|
-
it("
|
|
198
|
-
// Create a bundle via handler
|
|
254
|
+
it("creates a bundle through handler POST /bundles", async () => {
|
|
199
255
|
const bundleId = uuidv7();
|
|
200
|
-
const
|
|
256
|
+
const response = await api.handlers.admin(
|
|
257
|
+
new Request(`${baseUrl}/bundles`, {
|
|
258
|
+
body: JSON.stringify(
|
|
259
|
+
createTestBundle({
|
|
260
|
+
id: bundleId,
|
|
261
|
+
fileHash: "integration-hash-1",
|
|
262
|
+
}),
|
|
263
|
+
),
|
|
264
|
+
headers: { "Content-Type": "application/json" },
|
|
265
|
+
method: "POST",
|
|
266
|
+
}),
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
const retrieved = await api.handlers.admin(
|
|
270
|
+
new Request(`${baseUrl}/bundles/${bundleId}`),
|
|
271
|
+
);
|
|
272
|
+
expect(response.status).toBe(201);
|
|
273
|
+
expect(retrieved.status).toBe(200);
|
|
274
|
+
await expect(retrieved.json()).resolves.toMatchObject({
|
|
201
275
|
id: bundleId,
|
|
202
|
-
fileHash: "
|
|
276
|
+
fileHash: "integration-hash-1",
|
|
203
277
|
});
|
|
278
|
+
await expect(api.getReleases({ bundleId, limit: 10 })).resolves.toEqual([]);
|
|
279
|
+
});
|
|
204
280
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
// Create standalone repository
|
|
212
|
-
const repo = standaloneRepository({
|
|
213
|
-
baseUrl: `${baseUrl}/hot-updater`,
|
|
214
|
-
})();
|
|
281
|
+
it("retrieves a bundle through handler GET /bundles/:id", async () => {
|
|
282
|
+
const bundleId = uuidv7();
|
|
283
|
+
await api.insertBundle(
|
|
284
|
+
createTestBundle({ id: bundleId, fileHash: "get-hash-1" }),
|
|
285
|
+
);
|
|
215
286
|
|
|
216
|
-
|
|
217
|
-
await repo.deleteBundle(bundle);
|
|
218
|
-
await repo.commitBundle();
|
|
287
|
+
const retrieved = await createStandaloneClient().getBundleById(bundleId);
|
|
219
288
|
|
|
220
|
-
|
|
221
|
-
const afterDelete = await api.getBundleById(bundleId);
|
|
222
|
-
expect(afterDelete).toBeNull();
|
|
289
|
+
expect(retrieved).toMatchObject({ id: bundleId, fileHash: "get-hash-1" });
|
|
223
290
|
});
|
|
224
291
|
|
|
225
|
-
it("
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
);
|
|
230
|
-
await api.insertBundle(
|
|
231
|
-
createTestBundle({ id: uuidv7(), channel: "production" }),
|
|
232
|
-
);
|
|
233
|
-
await api.insertBundle(
|
|
234
|
-
createTestBundle({ id: uuidv7(), channel: "staging" }),
|
|
235
|
-
);
|
|
292
|
+
it("refuses to delete an artifact referenced by a Release", async () => {
|
|
293
|
+
const bundleId = uuidv7();
|
|
294
|
+
const bundle = createTestBundle({ id: bundleId });
|
|
295
|
+
await commitTestRelease(bundle);
|
|
296
|
+
const client = createStandaloneClient();
|
|
236
297
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
})();
|
|
298
|
+
await expect(client.deleteBundleById(bundleId)).rejects.toThrow(
|
|
299
|
+
"still referenced",
|
|
300
|
+
);
|
|
241
301
|
|
|
242
|
-
|
|
243
|
-
|
|
302
|
+
await expect(api.getBundleById(bundleId)).resolves.toMatchObject({
|
|
303
|
+
id: bundleId,
|
|
304
|
+
});
|
|
305
|
+
});
|
|
244
306
|
|
|
245
|
-
|
|
246
|
-
|
|
307
|
+
it("lists and filters artifacts through handler GET /bundles", async () => {
|
|
308
|
+
await api.insertBundle(createTestBundle({ id: uuidv7() }));
|
|
309
|
+
await api.insertBundle(createTestBundle({ id: uuidv7() }));
|
|
310
|
+
await api.insertBundle(createTestBundle({ id: uuidv7() }));
|
|
311
|
+
const client = createStandaloneClient();
|
|
247
312
|
|
|
248
|
-
|
|
249
|
-
const
|
|
250
|
-
where: {
|
|
313
|
+
const all = await client.getBundles({ limit: 50 });
|
|
314
|
+
const ios = await client.getBundles({
|
|
315
|
+
where: { platform: "ios" },
|
|
251
316
|
limit: 50,
|
|
252
317
|
});
|
|
253
318
|
|
|
254
|
-
expect(
|
|
319
|
+
expect(all.data).toHaveLength(3);
|
|
320
|
+
expect(all.pagination.total).toBe(3);
|
|
321
|
+
expect(ios.data).toHaveLength(3);
|
|
255
322
|
});
|
|
256
323
|
|
|
257
|
-
it("
|
|
258
|
-
await api.
|
|
259
|
-
|
|
324
|
+
it("lists Channel rows through handler GET /channels", async () => {
|
|
325
|
+
await api.insertChannel({
|
|
326
|
+
onConflict: "returnExisting",
|
|
327
|
+
row: { id: uuidv7(), name: "production" },
|
|
328
|
+
});
|
|
329
|
+
await api.insertChannel({
|
|
330
|
+
onConflict: "returnExisting",
|
|
331
|
+
row: { id: uuidv7(), name: "beta" },
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
const channels = await createStandaloneClient().getChannels();
|
|
335
|
+
|
|
336
|
+
expect(channels.map(({ name }) => name)).toEqual(
|
|
337
|
+
expect.arrayContaining(["production", "beta"]),
|
|
260
338
|
);
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
339
|
+
expect(channels.every(({ id }) => id.length > 0)).toBe(true);
|
|
340
|
+
expect(new Set(channels.map(({ name }) => name)).size).toBe(
|
|
341
|
+
channels.length,
|
|
264
342
|
);
|
|
343
|
+
});
|
|
265
344
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const channels = await repo.getChannels();
|
|
345
|
+
it("creates and deletes an empty Channel through the canonical routes", async () => {
|
|
346
|
+
const client = createStandaloneClient();
|
|
347
|
+
const id = uuidv7();
|
|
271
348
|
|
|
272
|
-
expect(
|
|
273
|
-
|
|
274
|
-
|
|
349
|
+
await expect(
|
|
350
|
+
client.insertChannel({
|
|
351
|
+
row: { id, name: "preview" },
|
|
352
|
+
onConflict: "returnExisting",
|
|
353
|
+
}),
|
|
354
|
+
).resolves.toEqual({
|
|
355
|
+
row: { id, name: "preview" },
|
|
356
|
+
inserted: true,
|
|
357
|
+
});
|
|
358
|
+
await expect(client.deleteChannel({ id })).resolves.toEqual({
|
|
359
|
+
deleted: true,
|
|
360
|
+
});
|
|
361
|
+
await expect(client.deleteChannel({ id })).resolves.toEqual({
|
|
362
|
+
deleted: false,
|
|
363
|
+
reason: "not_found",
|
|
364
|
+
});
|
|
275
365
|
});
|
|
276
366
|
|
|
277
|
-
it("
|
|
278
|
-
const
|
|
279
|
-
baseUrl: `${baseUrl}/hot-updater`,
|
|
280
|
-
})();
|
|
281
|
-
|
|
282
|
-
// Step 1: Create bundle via standalone
|
|
367
|
+
it("refuses to delete a Channel referenced by a Release", async () => {
|
|
368
|
+
const client = createStandaloneClient();
|
|
283
369
|
const bundleId = uuidv7();
|
|
284
|
-
|
|
370
|
+
await commitTestRelease(createTestBundle({ id: bundleId }), "preview");
|
|
371
|
+
const channel = (await client.getChannels()).find(
|
|
372
|
+
({ name }) => name === "preview",
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
expect(channel).toBeDefined();
|
|
376
|
+
if (!channel) throw new Error("preview Channel was not created");
|
|
377
|
+
await expect(client.deleteChannel({ id: channel.id })).resolves.toEqual({
|
|
378
|
+
deleted: false,
|
|
379
|
+
reason: "not_empty",
|
|
380
|
+
});
|
|
381
|
+
await expect(client.getBundleById(bundleId)).resolves.toMatchObject({
|
|
285
382
|
id: bundleId,
|
|
286
|
-
fileHash: "e2e-hash",
|
|
287
|
-
enabled: true,
|
|
288
383
|
});
|
|
384
|
+
});
|
|
289
385
|
|
|
290
|
-
|
|
291
|
-
|
|
386
|
+
it("updates an artifact without changing its Release or catalog", async () => {
|
|
387
|
+
const bundle = createTestBundle({ id: uuidv7() });
|
|
388
|
+
const { releaseId, repository, scopeKey } = await commitTestRelease(bundle);
|
|
389
|
+
const releaseBefore = await repository.models.releases.findById(releaseId);
|
|
390
|
+
const catalogBefore =
|
|
391
|
+
await repository.models.releaseCatalogs.findByScopeKey(scopeKey);
|
|
292
392
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
expect(retrieved?.enabled).toBe(true);
|
|
393
|
+
await createStandaloneClient().updateBundleById(bundle.id, {
|
|
394
|
+
storageUri: "test://updated-artifact",
|
|
395
|
+
});
|
|
297
396
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
397
|
+
await expect(api.getBundleById(bundle.id)).resolves.toMatchObject({
|
|
398
|
+
storageUri: "test://updated-artifact",
|
|
399
|
+
});
|
|
400
|
+
await expect(
|
|
401
|
+
repository.models.releases.findById(releaseId),
|
|
402
|
+
).resolves.toEqual(releaseBefore);
|
|
403
|
+
await expect(
|
|
404
|
+
repository.models.releaseCatalogs.findByScopeKey(scopeKey),
|
|
405
|
+
).resolves.toEqual(catalogBefore);
|
|
406
|
+
});
|
|
301
407
|
|
|
302
|
-
|
|
303
|
-
const
|
|
304
|
-
|
|
408
|
+
it("creates, retrieves, updates, and deletes through existing routes", async () => {
|
|
409
|
+
const client = createStandaloneClient();
|
|
410
|
+
const bundleId = uuidv7();
|
|
411
|
+
await client.insertBundle(
|
|
412
|
+
createTestBundle({ id: bundleId, fileHash: "e2e-hash" }),
|
|
413
|
+
);
|
|
305
414
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
415
|
+
await expect(client.getBundleById(bundleId)).resolves.toMatchObject({
|
|
416
|
+
fileHash: "e2e-hash",
|
|
417
|
+
});
|
|
418
|
+
await client.updateBundleById(bundleId, { storageUri: "test://updated" });
|
|
419
|
+
await expect(client.getBundleById(bundleId)).resolves.toMatchObject({
|
|
420
|
+
storageUri: "test://updated",
|
|
421
|
+
});
|
|
422
|
+
await client.deleteBundleById(bundleId);
|
|
423
|
+
await expect(client.getBundleById(bundleId)).resolves.toBeNull();
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it("creates multiple artifacts atomically through POST /bundles", async () => {
|
|
427
|
+
const ids = [uuidv7(), uuidv7(), uuidv7()];
|
|
309
428
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
429
|
+
const response = await api.handlers.admin(
|
|
430
|
+
new Request(`${baseUrl}/bundles`, {
|
|
431
|
+
body: JSON.stringify(ids.map((id) => createTestBundle({ id }))),
|
|
432
|
+
headers: { "Content-Type": "application/json" },
|
|
433
|
+
method: "POST",
|
|
434
|
+
}),
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
expect(response.status).toBe(201);
|
|
438
|
+
for (const id of ids) {
|
|
439
|
+
await expect(api.getBundleById(id)).resolves.toMatchObject({ id });
|
|
440
|
+
}
|
|
313
441
|
});
|
|
314
442
|
|
|
315
|
-
it("
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
})
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
const bundle2 = await api.getBundleById(bundleId2);
|
|
334
|
-
const bundle3 = await api.getBundleById(bundleId3);
|
|
335
|
-
|
|
336
|
-
expect(bundle1).toBeTruthy();
|
|
337
|
-
expect(bundle2).toBeTruthy();
|
|
338
|
-
expect(bundle3).toBeTruthy();
|
|
443
|
+
it("rolls back the entire create batch when one bundle cannot be inserted", async () => {
|
|
444
|
+
const existingId = uuidv7();
|
|
445
|
+
const newId = uuidv7();
|
|
446
|
+
await api.insertBundle(createTestBundle({ id: existingId }));
|
|
447
|
+
|
|
448
|
+
const response = await api.handlers.admin(
|
|
449
|
+
new Request(`${baseUrl}/bundles`, {
|
|
450
|
+
method: "POST",
|
|
451
|
+
headers: { "Content-Type": "application/json" },
|
|
452
|
+
body: JSON.stringify([
|
|
453
|
+
createTestBundle({ id: newId }),
|
|
454
|
+
createTestBundle({ id: existingId }),
|
|
455
|
+
]),
|
|
456
|
+
}),
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
expect(response.status).toBe(500);
|
|
460
|
+
await expect(api.getBundleById(newId)).resolves.toBeNull();
|
|
339
461
|
});
|
|
340
462
|
|
|
341
|
-
it("
|
|
342
|
-
// Create handler with custom basePath
|
|
463
|
+
it("works when handlers are mounted under a custom client path", async () => {
|
|
343
464
|
const customApi = createHotUpdater({
|
|
344
|
-
database: kyselyAdapter({
|
|
345
|
-
|
|
346
|
-
provider: "postgresql",
|
|
347
|
-
}),
|
|
348
|
-
basePath: "/api/v2",
|
|
349
|
-
routes: {
|
|
350
|
-
updateCheck: true,
|
|
351
|
-
bundles: true,
|
|
352
|
-
},
|
|
465
|
+
database: kyselyAdapter({ db: kysely, provider: "postgresql" }),
|
|
466
|
+
clientAccess: { type: "public" },
|
|
353
467
|
});
|
|
354
|
-
|
|
355
|
-
// Setup MSW for custom basePath
|
|
356
468
|
server.use(
|
|
357
|
-
http.
|
|
358
|
-
|
|
359
|
-
return HttpResponse.json(
|
|
360
|
-
(await response.json()) as Record<string, unknown>,
|
|
361
|
-
{
|
|
362
|
-
status: response.status,
|
|
363
|
-
},
|
|
364
|
-
);
|
|
365
|
-
}),
|
|
366
|
-
http.post(`${baseUrl}/api/v2/*`, async ({ request }) => {
|
|
367
|
-
const response = await customApi.handler(request);
|
|
368
|
-
return HttpResponse.json(
|
|
369
|
-
(await response.json()) as Record<string, unknown>,
|
|
370
|
-
{
|
|
371
|
-
status: response.status,
|
|
372
|
-
},
|
|
373
|
-
);
|
|
374
|
-
}),
|
|
375
|
-
http.patch(`${baseUrl}/api/v2/*`, async ({ request }) => {
|
|
376
|
-
const response = await customApi.handler(request);
|
|
377
|
-
return HttpResponse.json(
|
|
378
|
-
(await response.json()) as Record<string, unknown>,
|
|
379
|
-
{
|
|
380
|
-
status: response.status,
|
|
381
|
-
},
|
|
382
|
-
);
|
|
469
|
+
http.all(`${baseUrl}/api/v2/*`, async ({ request }) => {
|
|
470
|
+
return invokeHandler(customApi, request, "/api/v2");
|
|
383
471
|
}),
|
|
384
472
|
);
|
|
473
|
+
const bundleId = uuidv7();
|
|
474
|
+
const client = createStandaloneClient(`${baseUrl}/api/v2/admin`);
|
|
385
475
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
})();
|
|
476
|
+
await client.insertBundle(
|
|
477
|
+
createTestBundle({ id: bundleId, fileHash: "custom-hash" }),
|
|
478
|
+
);
|
|
390
479
|
|
|
391
|
-
|
|
392
|
-
const bundleId = uuidv7();
|
|
393
|
-
const bundle = createTestBundle({
|
|
394
|
-
id: bundleId,
|
|
480
|
+
await expect(client.getBundleById(bundleId)).resolves.toMatchObject({
|
|
395
481
|
fileHash: "custom-hash",
|
|
396
482
|
});
|
|
397
|
-
|
|
398
|
-
await repo.appendBundle(bundle);
|
|
399
|
-
await repo.commitBundle();
|
|
400
|
-
|
|
401
|
-
const retrieved = await repo.getBundleById(bundleId);
|
|
402
|
-
expect(retrieved).toBeTruthy();
|
|
403
|
-
expect(retrieved?.fileHash).toBe("custom-hash");
|
|
404
483
|
});
|
|
405
484
|
|
|
406
|
-
it("
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
// Try to get non-existent bundle
|
|
412
|
-
const result = await repo.getBundleById("non-existent-bundle");
|
|
413
|
-
|
|
414
|
-
// Standalone should return null gracefully
|
|
415
|
-
expect(result).toBeNull();
|
|
485
|
+
it("returns null when the bundle endpoint returns 404", async () => {
|
|
486
|
+
await expect(
|
|
487
|
+
createStandaloneClient().getBundleById(uuidv7()),
|
|
488
|
+
).resolves.toBeNull();
|
|
416
489
|
});
|
|
417
490
|
|
|
418
|
-
it("updates
|
|
419
|
-
const
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
basePath: "/blob-hot-updater",
|
|
423
|
-
routes: {
|
|
424
|
-
updateCheck: true,
|
|
425
|
-
bundles: true,
|
|
426
|
-
},
|
|
491
|
+
it("updates a bundle without creating a duplicate row", async () => {
|
|
492
|
+
const memoryApi = createHotUpdater({
|
|
493
|
+
database: createInMemoryDatabasePlugin(),
|
|
494
|
+
clientAccess: { type: "public" },
|
|
427
495
|
});
|
|
428
|
-
const handleBlobRequest = async (request: Request) => {
|
|
429
|
-
const response = await blobApi.handler(request);
|
|
430
|
-
return HttpResponse.json(
|
|
431
|
-
(await response.json()) as Record<string, unknown>,
|
|
432
|
-
{
|
|
433
|
-
status: response.status,
|
|
434
|
-
headers: response.headers,
|
|
435
|
-
},
|
|
436
|
-
);
|
|
437
|
-
};
|
|
438
|
-
|
|
439
496
|
server.use(
|
|
440
|
-
http.
|
|
441
|
-
|
|
442
|
-
),
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
http.post(`${baseUrl}/blob-hot-updater/api/bundles`, ({ request }) =>
|
|
447
|
-
handleBlobRequest(request),
|
|
448
|
-
),
|
|
449
|
-
http.patch(`${baseUrl}/blob-hot-updater/api/bundles/:id`, ({ request }) =>
|
|
450
|
-
handleBlobRequest(request),
|
|
451
|
-
),
|
|
452
|
-
http.delete(
|
|
453
|
-
`${baseUrl}/blob-hot-updater/api/bundles/:id`,
|
|
454
|
-
({ request }) => handleBlobRequest(request),
|
|
455
|
-
),
|
|
497
|
+
http.all(`${baseUrl}/memory-hot-updater/*`, async ({ request }) => {
|
|
498
|
+
return invokeHandler(memoryApi, request, "/memory-hot-updater");
|
|
499
|
+
}),
|
|
500
|
+
);
|
|
501
|
+
const client = createStandaloneClient(
|
|
502
|
+
`${baseUrl}/memory-hot-updater/admin`,
|
|
456
503
|
);
|
|
457
|
-
|
|
458
|
-
const repo = standaloneRepository({
|
|
459
|
-
baseUrl: `${baseUrl}/blob-hot-updater`,
|
|
460
|
-
})();
|
|
461
|
-
|
|
462
504
|
const bundleId = uuidv7();
|
|
463
|
-
await
|
|
505
|
+
await client.insertBundle(
|
|
464
506
|
createTestBundle({
|
|
465
507
|
id: bundleId,
|
|
466
|
-
platform: "ios",
|
|
467
|
-
targetAppVersion: "1.x.x",
|
|
468
508
|
storageUri: "s3://test-bucket/original.zip",
|
|
469
509
|
}),
|
|
470
510
|
);
|
|
471
|
-
await repo.commitBundle();
|
|
472
|
-
|
|
473
|
-
await repo.updateBundle(bundleId, { targetAppVersion: "1.0.2" });
|
|
474
|
-
await repo.commitBundle();
|
|
475
|
-
|
|
476
|
-
const updatedBundle = await repo.getBundleById(bundleId);
|
|
477
|
-
expect(updatedBundle?.targetAppVersion).toBe("1.0.2");
|
|
478
|
-
|
|
479
|
-
expect(store["production/ios/1.x.x/update.json"]).toBeUndefined();
|
|
480
511
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
)
|
|
484
|
-
expect(nextBundles).toHaveLength(1);
|
|
485
|
-
expect(nextBundles[0]?.id).toBe(bundleId);
|
|
486
|
-
expect(nextBundles[0]?.targetAppVersion).toBe("1.0.2");
|
|
512
|
+
await client.updateBundleById(bundleId, {
|
|
513
|
+
storageUri: "s3://test-bucket/updated.zip",
|
|
514
|
+
});
|
|
487
515
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
516
|
+
await expect(client.getBundleById(bundleId)).resolves.toMatchObject({
|
|
517
|
+
id: bundleId,
|
|
518
|
+
storageUri: "s3://test-bucket/updated.zip",
|
|
519
|
+
});
|
|
520
|
+
await expect(client.getBundles({ limit: 50 })).resolves.toMatchObject({
|
|
521
|
+
data: [
|
|
522
|
+
expect.objectContaining({
|
|
523
|
+
id: bundleId,
|
|
524
|
+
storageUri: "s3://test-bucket/updated.zip",
|
|
525
|
+
}),
|
|
526
|
+
],
|
|
527
|
+
pagination: { total: 1 },
|
|
528
|
+
});
|
|
492
529
|
});
|
|
493
530
|
});
|