@hot-updater/server 1.0.0-rc.2 → 1.0.0-rc.21
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/LICENSE +12 -14
- package/dist/adapters/drizzle.cjs +32 -75
- package/dist/adapters/drizzle.d.cts +13 -5
- package/dist/adapters/drizzle.d.mts +13 -5
- package/dist/adapters/drizzle.mjs +32 -77
- package/dist/adapters/drizzleExecutor.cjs +114 -0
- package/dist/adapters/drizzleExecutor.d.cts +9 -0
- package/dist/adapters/drizzleExecutor.d.mts +9 -0
- package/dist/adapters/drizzleExecutor.mjs +112 -0
- package/dist/adapters/kysely.cjs +22 -31
- package/dist/adapters/kysely.d.cts +10 -6
- package/dist/adapters/kysely.d.mts +10 -6
- package/dist/adapters/kysely.mjs +23 -33
- package/dist/adapters/kyselyExecutor.cjs +32 -0
- package/dist/adapters/kyselyExecutor.d.cts +11 -0
- package/dist/adapters/kyselyExecutor.d.mts +11 -0
- package/dist/adapters/kyselyExecutor.mjs +32 -0
- package/dist/adapters/mongodb.cjs +18 -54
- package/dist/adapters/mongodb.d.cts +10 -5
- package/dist/adapters/mongodb.d.mts +10 -5
- package/dist/adapters/mongodb.mjs +19 -56
- package/dist/adapters/mongodbAdapter.cjs +192 -0
- package/dist/adapters/mongodbAdapter.d.cts +9 -0
- package/dist/adapters/mongodbAdapter.d.mts +9 -0
- package/dist/adapters/mongodbAdapter.mjs +191 -0
- package/dist/adapters/prisma.cjs +35 -267
- package/dist/adapters/prisma.d.cts +12 -7
- package/dist/adapters/prisma.d.mts +12 -7
- package/dist/adapters/prisma.mjs +36 -268
- package/dist/adapters/prismaExecutor.cjs +63 -0
- package/dist/adapters/prismaExecutor.mjs +63 -0
- package/dist/adapters/sqlProviders.cjs +14 -0
- package/dist/adapters/sqlProviders.mjs +14 -0
- package/dist/assembly/assemblePlugins.cjs +144 -0
- package/dist/assembly/assemblePlugins.d.cts +5 -0
- package/dist/assembly/assemblePlugins.d.mts +5 -0
- package/dist/assembly/assemblePlugins.mjs +139 -0
- package/dist/assembly/clientPlugins.cjs +88 -0
- package/dist/assembly/clientPlugins.mjs +88 -0
- package/dist/assembly/configError.d.cts +2 -0
- package/dist/assembly/configError.d.mts +2 -0
- package/dist/assembly/configError.mjs +2 -0
- package/dist/core/api.cjs +51 -0
- package/dist/core/api.d.cts +54 -0
- package/dist/core/api.d.mts +54 -0
- package/dist/core/api.mjs +51 -0
- package/dist/core/baseCandidates.cjs +28 -0
- package/dist/core/baseCandidates.mjs +28 -0
- package/dist/core/operations.cjs +351 -0
- package/dist/core/operations.mjs +351 -0
- package/dist/core/reads.cjs +273 -0
- package/dist/core/reads.d.cts +9 -0
- package/dist/core/reads.d.mts +9 -0
- package/dist/core/reads.mjs +267 -0
- package/dist/core/releases.cjs +193 -0
- package/dist/core/releases.mjs +190 -0
- package/dist/core/writes.cjs +96 -0
- package/dist/core/writes.mjs +91 -0
- package/dist/createHotUpdaterCore.cjs +72 -91
- package/dist/createHotUpdaterCore.d.cts +106 -38
- package/dist/createHotUpdaterCore.d.mts +106 -38
- package/dist/createHotUpdaterCore.mjs +74 -89
- package/dist/db/engineDrizzleSchema.cjs +53 -0
- package/dist/db/engineDrizzleSchema.mjs +53 -0
- package/dist/db/enginePrismaSchema.cjs +84 -0
- package/dist/db/enginePrismaSchema.d.cts +5 -0
- package/dist/db/enginePrismaSchema.d.mts +5 -0
- package/dist/db/enginePrismaSchema.mjs +83 -0
- package/dist/db/releaseCatalog.cjs +24 -47
- package/dist/db/releaseCatalog.mjs +20 -44
- package/dist/db/updateArtifacts.cjs +43 -72
- package/dist/db/updateArtifacts.mjs +40 -69
- package/dist/handler.cjs +59 -43
- package/dist/handler.d.cts +3 -5
- package/dist/handler.d.mts +3 -5
- package/dist/handler.mjs +61 -43
- package/dist/handlerAdminRoutes.cjs +374 -0
- package/dist/handlerAdminRoutes.mjs +372 -0
- package/dist/handlerParameters.cjs +6 -11
- package/dist/handlerParameters.mjs +6 -10
- package/dist/handlerReleaseCatalogRoutes.cjs +32 -15
- package/dist/handlerReleaseCatalogRoutes.mjs +32 -15
- package/dist/handlerTypes.d.cts +2 -55
- package/dist/handlerTypes.d.mts +2 -55
- package/dist/handlerVersionRoutes.cjs +20 -4
- package/dist/handlerVersionRoutes.mjs +20 -4
- package/dist/index.cjs +9 -9
- package/dist/index.d.cts +8 -9
- package/dist/index.d.mts +8 -9
- package/dist/index.mjs +3 -4
- package/dist/node.cjs +3 -6
- package/dist/node.d.cts +2 -2
- package/dist/node.d.mts +2 -2
- package/dist/node.mjs +4 -4
- package/dist/package.cjs +1 -1
- package/dist/package.mjs +1 -1
- package/dist/plugins/api-keys/index.cjs +9 -0
- package/dist/plugins/api-keys/index.d.cts +1 -0
- package/dist/plugins/api-keys/index.d.mts +1 -0
- package/dist/plugins/api-keys/index.mjs +2 -0
- package/dist/plugins/definePlugin.d.cts +2 -0
- package/dist/plugins/definePlugin.d.mts +2 -0
- package/dist/plugins/insights/index.cjs +9 -0
- package/dist/plugins/insights/index.d.cts +1 -0
- package/dist/plugins/insights/index.d.mts +1 -0
- package/dist/plugins/insights/index.mjs +2 -0
- package/dist/plugins/official.cjs +30 -0
- package/dist/plugins/official.mjs +29 -0
- package/dist/storageAccess.cjs +13 -10
- package/dist/storageAccess.mjs +13 -10
- package/dist/types/index.d.cts +2 -28
- package/dist/types/index.d.mts +2 -28
- package/package.json +26 -14
- package/dist/_virtual/_rolldown/runtime.cjs +0 -23
- package/dist/adapters/databaseConstraintErrors.cjs +0 -22
- package/dist/adapters/databaseConstraintErrors.mjs +0 -21
- package/dist/adapters/databasePluginUtils.cjs +0 -116
- package/dist/adapters/databasePluginUtils.mjs +0 -106
- package/dist/adapters/drizzleCrud.cjs +0 -311
- package/dist/adapters/drizzleCrud.mjs +0 -308
- package/dist/adapters/drizzleLazyDB.cjs +0 -106
- package/dist/adapters/drizzleLazyDB.mjs +0 -106
- package/dist/adapters/drizzleQuery.cjs +0 -69
- package/dist/adapters/drizzleQuery.mjs +0 -69
- package/dist/adapters/insightsTransaction.cjs +0 -22
- package/dist/adapters/insightsTransaction.mjs +0 -22
- package/dist/adapters/kyselyCrud.cjs +0 -305
- package/dist/adapters/kyselyCrud.mjs +0 -304
- package/dist/adapters/kyselyQuery.cjs +0 -56
- package/dist/adapters/kyselyQuery.mjs +0 -56
- package/dist/adapters/mongodbCollections.cjs +0 -38
- package/dist/adapters/mongodbCollections.mjs +0 -32
- package/dist/adapters/mongodbQuery.cjs +0 -94
- package/dist/adapters/mongodbQuery.mjs +0 -86
- package/dist/adapters/mongodbReads.cjs +0 -210
- package/dist/adapters/mongodbReads.mjs +0 -210
- package/dist/adapters/mongodbWrites.cjs +0 -148
- package/dist/adapters/mongodbWrites.mjs +0 -148
- package/dist/adapters/prismaQuery.cjs +0 -57
- package/dist/adapters/prismaQuery.mjs +0 -56
- package/dist/adapters/prismaRows.cjs +0 -219
- package/dist/adapters/prismaRows.mjs +0 -209
- package/dist/apiKeys.cjs +0 -143
- package/dist/apiKeys.d.cts +0 -49
- package/dist/apiKeys.d.mts +0 -49
- package/dist/apiKeys.mjs +0 -133
- package/dist/db/createBundleDiff.cjs +0 -144
- package/dist/db/createBundleDiff.d.cts +0 -16
- package/dist/db/createBundleDiff.d.mts +0 -16
- package/dist/db/createBundleDiff.mjs +0 -141
- package/dist/db/databasePluginCore.cjs +0 -137
- package/dist/db/databasePluginCore.mjs +0 -137
- package/dist/db/fixedMigrator.cjs +0 -79
- package/dist/db/fixedMigrator.mjs +0 -78
- package/dist/db/fixedMigratorMongo.cjs +0 -321
- package/dist/db/fixedMigratorMongo.mjs +0 -321
- package/dist/db/fixedMigratorShared.cjs +0 -23
- package/dist/db/fixedMigratorShared.mjs +0 -18
- package/dist/db/index.cjs +0 -27
- package/dist/db/index.d.cts +0 -12
- package/dist/db/index.d.mts +0 -12
- package/dist/db/index.mjs +0 -22
- package/dist/db/mongoMigrationExecution.cjs +0 -11
- package/dist/db/mongoMigrationExecution.mjs +0 -11
- package/dist/db/releaseCatalog.d.cts +0 -16
- package/dist/db/releaseCatalog.d.mts +0 -16
- package/dist/db/schema/insightsCollation.cjs +0 -14
- package/dist/db/schema/insightsCollation.mjs +0 -14
- package/dist/db/schema/mongodb.cjs +0 -25
- package/dist/db/schema/mongodb.mjs +0 -25
- package/dist/db/schema/registry.cjs +0 -21
- package/dist/db/schema/registry.mjs +0 -17
- package/dist/db/schema/sql.cjs +0 -105
- package/dist/db/schema/sql.mjs +0 -97
- package/dist/db/schema/sqlOperations.cjs +0 -27
- package/dist/db/schema/sqlOperations.mjs +0 -26
- package/dist/db/schemaGenerators.cjs +0 -275
- package/dist/db/schemaGenerators.mjs +0 -273
- package/dist/db/schemaReadiness.cjs +0 -26
- package/dist/db/schemaReadiness.d.cts +0 -9
- package/dist/db/schemaReadiness.d.mts +0 -9
- package/dist/db/schemaReadiness.mjs +0 -25
- package/dist/db/sqlMigrationExecution.cjs +0 -67
- package/dist/db/sqlMigrationExecution.mjs +0 -67
- package/dist/db/types.cjs +0 -22
- package/dist/db/types.d.cts +0 -118
- package/dist/db/types.d.mts +0 -118
- package/dist/db/types.mjs +0 -19
- package/dist/handlerBundleRoutes.cjs +0 -155
- package/dist/handlerBundleRoutes.mjs +0 -155
- package/dist/handlerReleaseManagementRoutes.cjs +0 -124
- package/dist/handlerReleaseManagementRoutes.mjs +0 -124
- package/dist/insights/domain.d.cts +0 -89
- package/dist/insights/domain.d.mts +0 -89
- package/dist/insights/errors.cjs +0 -15
- package/dist/insights/errors.mjs +0 -14
- package/dist/insights/eventInput.cjs +0 -155
- package/dist/insights/eventInput.mjs +0 -153
- package/dist/insights/provider.cjs +0 -314
- package/dist/insights/provider.d.cts +0 -6
- package/dist/insights/provider.d.mts +0 -6
- package/dist/insights/provider.mjs +0 -314
- package/dist/insights/queryInput.cjs +0 -93
- package/dist/insights/queryInput.mjs +0 -91
- package/dist/insights/routes.cjs +0 -58
- package/dist/insights/routes.mjs +0 -56
- package/dist/insights/types.d.cts +0 -32
- package/dist/insights/types.d.mts +0 -32
- package/dist/schema/dsl.cjs +0 -103
- package/dist/schema/dsl.mjs +0 -87
- package/dist/schema/index.cjs +0 -20
- package/dist/schema/index.mjs +0 -7
- package/dist/schema/settings.cjs +0 -9
- package/dist/schema/settings.mjs +0 -9
- package/dist/schema/types.cjs +0 -8
- package/dist/schema/types.mjs +0 -6
- package/dist/schema/v1_0_0.cjs +0 -406
- package/dist/schema/v1_0_0.mjs +0 -398
- package/src/adapters/databaseConstraintErrors.ts +0 -25
- package/src/adapters/databasePluginTestDatabase.ts +0 -120
- package/src/adapters/databasePluginUtils.spec.ts +0 -29
- package/src/adapters/databasePluginUtils.ts +0 -214
- package/src/adapters/drizzle.spec.ts +0 -336
- package/src/adapters/drizzle.ts +0 -162
- package/src/adapters/drizzleCrud.ts +0 -571
- package/src/adapters/drizzleLazyDB.ts +0 -271
- package/src/adapters/drizzleQuery.ts +0 -164
- package/src/adapters/insightsTransaction.ts +0 -32
- package/src/adapters/kysely.spec.ts +0 -174
- package/src/adapters/kysely.ts +0 -110
- package/src/adapters/kyselyCrud.ts +0 -709
- package/src/adapters/kyselyQuery.ts +0 -128
- package/src/adapters/mongodb.integration.spec.ts +0 -210
- package/src/adapters/mongodb.spec.ts +0 -209
- package/src/adapters/mongodb.ts +0 -138
- package/src/adapters/mongodbCollections.ts +0 -67
- package/src/adapters/mongodbQuery.ts +0 -255
- package/src/adapters/mongodbReads.ts +0 -380
- package/src/adapters/mongodbRows.spec.ts +0 -64
- package/src/adapters/mongodbRows.ts +0 -118
- package/src/adapters/mongodbTestClient.ts +0 -377
- package/src/adapters/mongodbTestFilter.ts +0 -169
- package/src/adapters/mongodbWrites.ts +0 -287
- package/src/adapters/prisma.spec.ts +0 -293
- package/src/adapters/prisma.ts +0 -565
- package/src/adapters/prismaQuery.ts +0 -129
- package/src/adapters/prismaRows.spec.ts +0 -98
- package/src/adapters/prismaRows.ts +0 -337
- package/src/adapters/prismaTestClient.ts +0 -413
- package/src/apiKeys.integration.spec.ts +0 -192
- package/src/apiKeys.spec.ts +0 -162
- package/src/apiKeys.ts +0 -220
- package/src/calculatePagination.ts +0 -34
- package/src/createHotUpdaterCore.ts +0 -246
- package/src/db/bundleRows.spec.ts +0 -164
- package/src/db/bundleRows.ts +0 -10
- package/src/db/createBundleDiff.spec.ts +0 -450
- package/src/db/createBundleDiff.ts +0 -364
- package/src/db/databasePluginCore.spec.ts +0 -62
- package/src/db/databasePluginCore.testFixtures.ts +0 -76
- package/src/db/databasePluginCore.ts +0 -185
- package/src/db/databasePluginUpdateCheck.bench.ts +0 -110
- package/src/db/fixedMigrator.spec.ts +0 -296
- package/src/db/fixedMigrator.ts +0 -147
- package/src/db/fixedMigratorMongo.spec.ts +0 -239
- package/src/db/fixedMigratorMongo.ts +0 -425
- package/src/db/fixedMigratorShared.ts +0 -36
- package/src/db/hotUpdaterSchema.ts +0 -5
- package/src/db/index.spec.ts +0 -1269
- package/src/db/index.ts +0 -47
- package/src/db/mongoMigrationExecution.ts +0 -23
- package/src/db/node-sqlite.d.ts +0 -25
- package/src/db/releaseCatalog.ts +0 -148
- package/src/db/schema/definitions.ts +0 -1
- package/src/db/schema/insightsCollation.ts +0 -21
- package/src/db/schema/mongodb.ts +0 -40
- package/src/db/schema/registry.ts +0 -55
- package/src/db/schema/sql.ts +0 -244
- package/src/db/schema/sqlOperations.ts +0 -63
- package/src/db/schema/types.ts +0 -1
- package/src/db/schemaGenerators.ts +0 -494
- package/src/db/schemaReadiness.ts +0 -34
- package/src/db/sqlMigrationExecution.spec.ts +0 -65
- package/src/db/sqlMigrationExecution.ts +0 -108
- package/src/db/types.spec.ts +0 -63
- package/src/db/types.ts +0 -228
- package/src/db/updateArtifacts.spec.ts +0 -359
- package/src/db/updateArtifacts.ts +0 -541
- package/src/handler-standalone.integration.spec.ts +0 -528
- package/src/handler.spec.ts +0 -190
- package/src/handler.testFixtures.ts +0 -30
- package/src/handler.ts +0 -245
- package/src/handlerAuth.integration.spec.ts +0 -33
- package/src/handlerBundleRoutes.ts +0 -259
- package/src/handlerErrors.ts +0 -7
- package/src/handlerManagement.spec.ts +0 -262
- package/src/handlerParameters.ts +0 -105
- package/src/handlerReleaseCatalogRoutes.ts +0 -154
- package/src/handlerReleaseManagementRoutes.ts +0 -213
- package/src/handlerTypes.ts +0 -96
- package/src/handlerVersionRoutes.ts +0 -12
- package/src/index.ts +0 -30
- package/src/insights/domain.ts +0 -103
- package/src/insights/errors.ts +0 -11
- package/src/insights/eventInput.ts +0 -225
- package/src/insights/insights.integration.spec.ts +0 -381
- package/src/insights/provider.spec.ts +0 -385
- package/src/insights/provider.ts +0 -591
- package/src/insights/queryInput.ts +0 -141
- package/src/insights/routes.ts +0 -104
- package/src/insights/types.ts +0 -55
- package/src/internalRouter.ts +0 -104
- package/src/node.spec.ts +0 -119
- package/src/node.ts +0 -111
- package/src/releaseCatalogRoutes.integration.spec.ts +0 -238
- package/src/runtime.spec.ts +0 -194
- package/src/runtime.testFixtures.ts +0 -71
- package/src/schema/dsl-all-versions.spec.ts +0 -37
- package/src/schema/dsl.ts +0 -187
- package/src/schema/index.ts +0 -10
- package/src/schema/settings.ts +0 -15
- package/src/schema/types.ts +0 -79
- package/src/schema/v1_0_0.spec.ts +0 -220
- package/src/schema/v1_0_0.ts +0 -529
- package/src/storageAccess.spec.ts +0 -152
- package/src/storageAccess.ts +0 -141
- package/src/types/index.ts +0 -34
- package/src/version.ts +0 -3
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
const require_reads = require("./reads.cjs");
|
|
2
|
+
const require_writes = require("./writes.cjs");
|
|
3
|
+
const require_releases = require("./releases.cjs");
|
|
4
|
+
let _hot_updater_plugin_core = require("@hot-updater/plugin-core");
|
|
5
|
+
let _hot_updater_protocol = require("@hot-updater/protocol");
|
|
6
|
+
//#region src/core/operations.ts
|
|
7
|
+
/** Carries a preflight's result out of the transaction it stops, so nothing is written. */
|
|
8
|
+
var Preflight = class extends Error {
|
|
9
|
+
result;
|
|
10
|
+
constructor(result) {
|
|
11
|
+
super("preflight");
|
|
12
|
+
this.result = result;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
const dryRun = async (db, read) => {
|
|
16
|
+
try {
|
|
17
|
+
await db.transaction(async (tx) => {
|
|
18
|
+
throw new Preflight(await read(tx));
|
|
19
|
+
});
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if (error instanceof Preflight) return error.result;
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
throw new Error("A preflight transaction returned without its result.");
|
|
25
|
+
};
|
|
26
|
+
/** A channel's catalog scope for a platform, and a fingerprint when it has one. */
|
|
27
|
+
const scopeOf = (channel, platform, fingerprintHash) => {
|
|
28
|
+
const channelKey = (0, _hot_updater_protocol.encodeChannelKey)(channel.name);
|
|
29
|
+
return fingerprintHash === null ? {
|
|
30
|
+
channelId: channel.id,
|
|
31
|
+
channelName: channel.name,
|
|
32
|
+
fingerprintHash,
|
|
33
|
+
platform,
|
|
34
|
+
scopeKey: (0, _hot_updater_protocol.createReleaseCatalogScopeKey)({
|
|
35
|
+
channelKey,
|
|
36
|
+
platform,
|
|
37
|
+
strategy: "APP_VERSION"
|
|
38
|
+
}),
|
|
39
|
+
strategy: "APP_VERSION"
|
|
40
|
+
} : {
|
|
41
|
+
channelId: channel.id,
|
|
42
|
+
channelName: channel.name,
|
|
43
|
+
fingerprintHash,
|
|
44
|
+
platform,
|
|
45
|
+
scopeKey: (0, _hot_updater_protocol.createReleaseCatalogScopeKey)({
|
|
46
|
+
channelKey,
|
|
47
|
+
fingerprintHash,
|
|
48
|
+
platform,
|
|
49
|
+
strategy: "FINGERPRINT"
|
|
50
|
+
}),
|
|
51
|
+
strategy: "FINGERPRINT"
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
/** A release and its scope, checked against the revision the caller saw. */
|
|
55
|
+
const loadTarget = async (tx, { releaseId, expectedRevision }) => {
|
|
56
|
+
const row = await tx.findOne("releases", { id: releaseId });
|
|
57
|
+
if (row === null) throw new _hot_updater_plugin_core.ReleaseManagementError("RELEASE_NOT_FOUND", `Release "${releaseId}" was not found.`);
|
|
58
|
+
const release = require_reads.toReleaseRow(row);
|
|
59
|
+
if (expectedRevision !== void 0 && expectedRevision !== release.revision) throw new _hot_updater_plugin_core.ReleaseManagementError("VERSION_CONFLICT", `Release "${release.id}" is revision ${release.revision}; expected ${expectedRevision}.`);
|
|
60
|
+
const catalog = await tx.findOne("release_catalogs", { scope_key: release.scope_key });
|
|
61
|
+
if (catalog === null) throw new _hot_updater_plugin_core.ReleaseCatalogMutationError("INVALID_SCOPE", `Release "${release.id}" has no catalog projection.`);
|
|
62
|
+
return {
|
|
63
|
+
release,
|
|
64
|
+
scope: {
|
|
65
|
+
channelId: release.channel_id,
|
|
66
|
+
channelName: (0, _hot_updater_protocol.decodeChannelKey)(catalog.channel_key),
|
|
67
|
+
fingerprintHash: release.fingerprint_hash,
|
|
68
|
+
platform: release.platform,
|
|
69
|
+
scopeKey: release.scope_key,
|
|
70
|
+
strategy: release.strategy
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
const policyUpdate = (release, patch, updatedAtMs) => {
|
|
75
|
+
if (patch.fingerprintHash !== void 0 && patch.fingerprintHash !== release.fingerprint_hash) throw new _hot_updater_plugin_core.ReleaseManagementError("SCOPE_MOVE_UNSUPPORTED", "Changing a fingerprint target requires an atomic Release scope move.");
|
|
76
|
+
return {
|
|
77
|
+
...patch.enabled === void 0 ? {} : { enabled: patch.enabled },
|
|
78
|
+
...patch.message === void 0 ? {} : { message: patch.message },
|
|
79
|
+
...patch.rolloutCohortCount === void 0 ? {} : { rollout_cohort_count: patch.rolloutCohortCount },
|
|
80
|
+
...patch.shouldForceUpdate === void 0 ? {} : { should_force_update: patch.shouldForceUpdate },
|
|
81
|
+
...patch.targetAppVersion === void 0 ? {} : { target_app_version: patch.targetAppVersion },
|
|
82
|
+
...patch.targetCohorts === void 0 ? {} : { target_cohorts: [...patch.targetCohorts] },
|
|
83
|
+
updated_at_ms: updatedAtMs
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
const mutationResult = ({ catalog, release }) => ({
|
|
87
|
+
attempts: 1,
|
|
88
|
+
catalog,
|
|
89
|
+
release
|
|
90
|
+
});
|
|
91
|
+
/** The scope a catalog row names, for a rebuild. */
|
|
92
|
+
const catalogScope = async (tx, scopeKey) => {
|
|
93
|
+
const row = await tx.findOne("release_catalogs", { scope_key: scopeKey });
|
|
94
|
+
if (row === null) throw new Error(`Release catalog "${scopeKey}" was not found.`);
|
|
95
|
+
return {
|
|
96
|
+
channelId: row.channel_id,
|
|
97
|
+
channelName: (0, _hot_updater_protocol.decodeChannelKey)(row.channel_key),
|
|
98
|
+
fingerprintHash: row.fingerprint_hash,
|
|
99
|
+
platform: row.platform,
|
|
100
|
+
scopeKey: row.scope_key,
|
|
101
|
+
strategy: row.strategy
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* Core's typed writes: each runs in one engine transaction rooted at the
|
|
106
|
+
* catalogs it changes, so a concurrent change reruns it instead of
|
|
107
|
+
* publishing a stale catalog.
|
|
108
|
+
*/
|
|
109
|
+
const createCoreOperations = (db, { now = Date.now } = {}) => {
|
|
110
|
+
const ensureChannel = async (name) => (await require_writes.insertChannel(db, {
|
|
111
|
+
id: `channel:${(0, _hot_updater_protocol.encodeChannelKey)(name)}`,
|
|
112
|
+
name
|
|
113
|
+
})).row;
|
|
114
|
+
/**
|
|
115
|
+
* Runs a write that ensured its channels first. A channel deleted between
|
|
116
|
+
* that and the write refuses the write, so it runs again and ensures it anew.
|
|
117
|
+
*/
|
|
118
|
+
const withChannels = async (write) => {
|
|
119
|
+
for (let attempt = 1;; attempt += 1) try {
|
|
120
|
+
return await write();
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (attempt < 3 && error instanceof _hot_updater_plugin_core.DatabaseConstraintError && error.reason === "not_found" && error.model === "channels") continue;
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
const deployOnce = async (deployments) => {
|
|
127
|
+
const channels = /* @__PURE__ */ new Map();
|
|
128
|
+
for (const { release } of deployments) if (!channels.has(release.channel)) channels.set(release.channel, await ensureChannel(release.channel));
|
|
129
|
+
const platforms = /* @__PURE__ */ new Map();
|
|
130
|
+
for (const deployment of deployments) {
|
|
131
|
+
if (!("bundleId" in deployment) || platforms.has(deployment.bundleId)) continue;
|
|
132
|
+
const row = await db.findOne("bundles", { id: deployment.bundleId });
|
|
133
|
+
if (row === null) throw new _hot_updater_plugin_core.DatabaseBundleNotFoundError(deployment.bundleId);
|
|
134
|
+
platforms.set(deployment.bundleId, require_reads.toBundleRow(row).platform);
|
|
135
|
+
}
|
|
136
|
+
const updatedAtMs = now();
|
|
137
|
+
return (await require_releases.changeReleases(db, deployments.map((deployment) => {
|
|
138
|
+
const { release: policy } = deployment;
|
|
139
|
+
const stored = "bundleId" in deployment;
|
|
140
|
+
const bundleId = stored ? deployment.bundleId : deployment.bundle.id;
|
|
141
|
+
const platform = stored ? platforms.get(deployment.bundleId) : deployment.bundle.platform;
|
|
142
|
+
const scope = scopeOf(channels.get(policy.channel), platform, policy.fingerprintHash);
|
|
143
|
+
return {
|
|
144
|
+
scope,
|
|
145
|
+
...stored ? {} : { bundle: {
|
|
146
|
+
row: (0, _hot_updater_plugin_core.bundleToRow)(deployment.bundle),
|
|
147
|
+
patches: (0, _hot_updater_plugin_core.bundleToPatchRows)(deployment.bundle)
|
|
148
|
+
} },
|
|
149
|
+
change: {
|
|
150
|
+
operation: "insert",
|
|
151
|
+
row: {
|
|
152
|
+
bundle_id: bundleId,
|
|
153
|
+
channel_id: scope.channelId,
|
|
154
|
+
created_at_ms: updatedAtMs,
|
|
155
|
+
enabled: policy.enabled,
|
|
156
|
+
fingerprint_hash: policy.fingerprintHash,
|
|
157
|
+
kind: "BUNDLE",
|
|
158
|
+
message: policy.message,
|
|
159
|
+
operation: "DEPLOY",
|
|
160
|
+
platform,
|
|
161
|
+
revision: 1,
|
|
162
|
+
rollout_cohort_count: policy.rolloutCohortCount ?? 1e3,
|
|
163
|
+
scope_key: scope.scopeKey,
|
|
164
|
+
should_force_update: policy.shouldForceUpdate,
|
|
165
|
+
source_release_id: null,
|
|
166
|
+
strategy: scope.strategy,
|
|
167
|
+
target_app_version: policy.targetAppVersion,
|
|
168
|
+
target_cohorts: policy.targetCohorts ?? [],
|
|
169
|
+
updated_at_ms: updatedAtMs
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
updatedAtMs
|
|
173
|
+
};
|
|
174
|
+
}))).map(mutationResult);
|
|
175
|
+
};
|
|
176
|
+
return {
|
|
177
|
+
deploy: (deployments) => withChannels(() => deployOnce(deployments)),
|
|
178
|
+
updateReleasePolicy: (input) => {
|
|
179
|
+
const updatedAtMs = now();
|
|
180
|
+
return db.transaction(async (tx) => {
|
|
181
|
+
const { release, scope } = await loadTarget(tx, input);
|
|
182
|
+
return mutationResult(await require_releases.changeRelease(tx, {
|
|
183
|
+
scope,
|
|
184
|
+
change: {
|
|
185
|
+
operation: "update",
|
|
186
|
+
id: release.id,
|
|
187
|
+
update: policyUpdate(release, input.patch, updatedAtMs)
|
|
188
|
+
},
|
|
189
|
+
updatedAtMs
|
|
190
|
+
}));
|
|
191
|
+
});
|
|
192
|
+
},
|
|
193
|
+
preflightReleasePolicy: (input) => {
|
|
194
|
+
const updatedAtMs = now();
|
|
195
|
+
return dryRun(db, async (tx) => {
|
|
196
|
+
const { release, scope } = await loadTarget(tx, input);
|
|
197
|
+
const stored = await require_releases.readCatalog(tx, scope.scopeKey);
|
|
198
|
+
const changed = await require_releases.changeRelease(tx, {
|
|
199
|
+
scope,
|
|
200
|
+
change: {
|
|
201
|
+
operation: "update",
|
|
202
|
+
id: release.id,
|
|
203
|
+
update: policyUpdate(release, input.patch, updatedAtMs)
|
|
204
|
+
},
|
|
205
|
+
updatedAtMs
|
|
206
|
+
});
|
|
207
|
+
return {
|
|
208
|
+
catalog: changed.catalog,
|
|
209
|
+
currentCatalog: stored.catalog,
|
|
210
|
+
diagnostics: changed.diagnostics,
|
|
211
|
+
expectedReleaseRevision: release.revision,
|
|
212
|
+
release: changed.release
|
|
213
|
+
};
|
|
214
|
+
});
|
|
215
|
+
},
|
|
216
|
+
deleteRelease: (input) => db.transaction(async (tx) => {
|
|
217
|
+
const { release, scope } = await loadTarget(tx, input);
|
|
218
|
+
if (release.enabled) throw new _hot_updater_plugin_core.ReleaseManagementError("ENABLED_RELEASE", `Disable Release "${release.id}" before hard deletion.`);
|
|
219
|
+
const bundle = release.bundle_id === null ? null : await tx.findOne("bundles", { id: release.bundle_id });
|
|
220
|
+
const result = mutationResult(await require_releases.changeRelease(tx, {
|
|
221
|
+
scope,
|
|
222
|
+
change: {
|
|
223
|
+
operation: "delete",
|
|
224
|
+
id: release.id
|
|
225
|
+
},
|
|
226
|
+
updatedAtMs: now()
|
|
227
|
+
}));
|
|
228
|
+
if (bundle !== null && (bundle._refs_releases_bundle_id ?? 0) <= 1) await require_writes.deleteBundle(tx, bundle);
|
|
229
|
+
return result;
|
|
230
|
+
}),
|
|
231
|
+
promoteRelease: (input) => withChannels(async () => {
|
|
232
|
+
const checkSource = (source) => {
|
|
233
|
+
if (source.kind !== "BUNDLE" || source.bundle_id === null) throw new _hot_updater_plugin_core.ReleaseManagementError("TARGET_RELEASE_INVALID", "Only a Bundle Release can be promoted.");
|
|
234
|
+
};
|
|
235
|
+
checkSource((await db.transaction((tx) => loadTarget(tx, input))).release);
|
|
236
|
+
const targetChannel = input.targetChannel.trim();
|
|
237
|
+
if (targetChannel.length === 0) throw new _hot_updater_plugin_core.ReleaseManagementError("TARGET_RELEASE_INVALID", "Promotion requires a target channel.");
|
|
238
|
+
const channel = await ensureChannel(targetChannel);
|
|
239
|
+
const updatedAtMs = now();
|
|
240
|
+
return db.transaction(async (tx) => {
|
|
241
|
+
const { release: source, scope: sourceScope } = await loadTarget(tx, input);
|
|
242
|
+
checkSource(source);
|
|
243
|
+
const targetScope = scopeOf(channel, source.platform, source.fingerprint_hash);
|
|
244
|
+
if (targetScope.scopeKey === sourceScope.scopeKey) throw new _hot_updater_plugin_core.ReleaseManagementError("TARGET_RELEASE_INVALID", "Source and target Release scopes are the same.");
|
|
245
|
+
const { rows } = await tx.findMany("releases", {
|
|
246
|
+
index: "byScope",
|
|
247
|
+
where: { scope_key: targetScope.scopeKey },
|
|
248
|
+
order: "desc",
|
|
249
|
+
limit: 1
|
|
250
|
+
});
|
|
251
|
+
const floor = [
|
|
252
|
+
rows[0]?.id ?? null,
|
|
253
|
+
source.id,
|
|
254
|
+
source.bundle_id
|
|
255
|
+
].filter((id) => id !== null && (0, _hot_updater_plugin_core.isUUIDv7)(id)).sort().at(-1);
|
|
256
|
+
const moved = input.action === "move" ? await require_releases.changeRelease(tx, {
|
|
257
|
+
scope: sourceScope,
|
|
258
|
+
change: {
|
|
259
|
+
operation: "update",
|
|
260
|
+
id: source.id,
|
|
261
|
+
update: {
|
|
262
|
+
enabled: false,
|
|
263
|
+
updated_at_ms: updatedAtMs
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
updatedAtMs
|
|
267
|
+
}) : null;
|
|
268
|
+
const target = await require_releases.changeRelease(tx, {
|
|
269
|
+
scope: targetScope,
|
|
270
|
+
change: {
|
|
271
|
+
operation: "insert",
|
|
272
|
+
row: {
|
|
273
|
+
...source,
|
|
274
|
+
id: (0, _hot_updater_plugin_core.createUUIDv7After)(floor ?? null, updatedAtMs),
|
|
275
|
+
revision: 1,
|
|
276
|
+
scope_key: targetScope.scopeKey,
|
|
277
|
+
channel_id: targetScope.channelId,
|
|
278
|
+
enabled: true,
|
|
279
|
+
rollout_cohort_count: 1e3,
|
|
280
|
+
target_cohorts: [],
|
|
281
|
+
operation: "PROMOTE",
|
|
282
|
+
source_release_id: source.id,
|
|
283
|
+
created_at_ms: updatedAtMs,
|
|
284
|
+
updated_at_ms: updatedAtMs
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
updatedAtMs
|
|
288
|
+
});
|
|
289
|
+
return {
|
|
290
|
+
source: moved === null ? null : mutationResult(moved),
|
|
291
|
+
target: mutationResult(target)
|
|
292
|
+
};
|
|
293
|
+
});
|
|
294
|
+
}),
|
|
295
|
+
rebuildReleaseCatalog: (scopeKey) => {
|
|
296
|
+
const updatedAtMs = now();
|
|
297
|
+
return db.transaction(async (tx) => {
|
|
298
|
+
const { current: _current, ...rebuilt } = await require_releases.rebuildScope(tx, await catalogScope(tx, scopeKey), updatedAtMs);
|
|
299
|
+
return {
|
|
300
|
+
attempts: 1,
|
|
301
|
+
...rebuilt
|
|
302
|
+
};
|
|
303
|
+
});
|
|
304
|
+
},
|
|
305
|
+
preflightReleaseCatalogRebuild: (scopeKey) => {
|
|
306
|
+
const updatedAtMs = now();
|
|
307
|
+
return dryRun(db, async (tx) => {
|
|
308
|
+
const rebuilt = await require_releases.rebuildScope(tx, await catalogScope(tx, scopeKey), updatedAtMs);
|
|
309
|
+
return {
|
|
310
|
+
changed: rebuilt.changed,
|
|
311
|
+
currentCatalog: rebuilt.current,
|
|
312
|
+
diagnostics: rebuilt.diagnostics,
|
|
313
|
+
projectedCatalog: rebuilt.catalog
|
|
314
|
+
};
|
|
315
|
+
});
|
|
316
|
+
},
|
|
317
|
+
ensureChannel,
|
|
318
|
+
deleteChannel: (id) => require_writes.deleteChannel(db, id),
|
|
319
|
+
updateBundle: (id, update) => db.transaction(async (tx) => {
|
|
320
|
+
const current = await tx.findOne("bundles", { id });
|
|
321
|
+
if (current === null) throw new _hot_updater_plugin_core.DatabaseBundleNotFoundError(id);
|
|
322
|
+
const { patches, ...fields } = update;
|
|
323
|
+
const next = {
|
|
324
|
+
...(0, _hot_updater_plugin_core.rowToBundle)(require_reads.toBundleRow(current)),
|
|
325
|
+
...fields,
|
|
326
|
+
id
|
|
327
|
+
};
|
|
328
|
+
const { id: _id, ...set } = (0, _hot_updater_plugin_core.bundleToRow)(next);
|
|
329
|
+
require_writes.updateBundle(tx, current, set);
|
|
330
|
+
if (patches !== void 0) await require_writes.replaceBundlePatches(tx, current, (0, _hot_updater_plugin_core.bundleToPatchRows)({
|
|
331
|
+
...next,
|
|
332
|
+
patches
|
|
333
|
+
}));
|
|
334
|
+
}),
|
|
335
|
+
deleteBundles: async (ids) => {
|
|
336
|
+
try {
|
|
337
|
+
await db.transaction(async (tx) => {
|
|
338
|
+
for (const id of new Set(ids)) {
|
|
339
|
+
const current = await tx.findOne("bundles", { id });
|
|
340
|
+
if (current !== null) await require_writes.deleteBundle(tx, current);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
} catch (error) {
|
|
344
|
+
if (error instanceof _hot_updater_plugin_core.DatabaseConstraintError && error.reason === "referenced") throw new _hot_updater_plugin_core.DatabaseRowReferencedError();
|
|
345
|
+
throw error;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
};
|
|
350
|
+
//#endregion
|
|
351
|
+
exports.createCoreOperations = createCoreOperations;
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import { toBundleRow, toReleaseRow } from "./reads.mjs";
|
|
2
|
+
import { deleteBundle, deleteChannel, insertChannel, replaceBundlePatches, updateBundle } from "./writes.mjs";
|
|
3
|
+
import { changeRelease, changeReleases, readCatalog, rebuildScope } from "./releases.mjs";
|
|
4
|
+
import { DatabaseBundleNotFoundError, DatabaseConstraintError, DatabaseRowReferencedError, ReleaseCatalogMutationError, ReleaseManagementError, bundleToPatchRows, bundleToRow, createUUIDv7After, isUUIDv7, rowToBundle } from "@hot-updater/plugin-core";
|
|
5
|
+
import { createReleaseCatalogScopeKey, decodeChannelKey, encodeChannelKey } from "@hot-updater/protocol";
|
|
6
|
+
//#region src/core/operations.ts
|
|
7
|
+
/** Carries a preflight's result out of the transaction it stops, so nothing is written. */
|
|
8
|
+
var Preflight = class extends Error {
|
|
9
|
+
result;
|
|
10
|
+
constructor(result) {
|
|
11
|
+
super("preflight");
|
|
12
|
+
this.result = result;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
const dryRun = async (db, read) => {
|
|
16
|
+
try {
|
|
17
|
+
await db.transaction(async (tx) => {
|
|
18
|
+
throw new Preflight(await read(tx));
|
|
19
|
+
});
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if (error instanceof Preflight) return error.result;
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
throw new Error("A preflight transaction returned without its result.");
|
|
25
|
+
};
|
|
26
|
+
/** A channel's catalog scope for a platform, and a fingerprint when it has one. */
|
|
27
|
+
const scopeOf = (channel, platform, fingerprintHash) => {
|
|
28
|
+
const channelKey = encodeChannelKey(channel.name);
|
|
29
|
+
return fingerprintHash === null ? {
|
|
30
|
+
channelId: channel.id,
|
|
31
|
+
channelName: channel.name,
|
|
32
|
+
fingerprintHash,
|
|
33
|
+
platform,
|
|
34
|
+
scopeKey: createReleaseCatalogScopeKey({
|
|
35
|
+
channelKey,
|
|
36
|
+
platform,
|
|
37
|
+
strategy: "APP_VERSION"
|
|
38
|
+
}),
|
|
39
|
+
strategy: "APP_VERSION"
|
|
40
|
+
} : {
|
|
41
|
+
channelId: channel.id,
|
|
42
|
+
channelName: channel.name,
|
|
43
|
+
fingerprintHash,
|
|
44
|
+
platform,
|
|
45
|
+
scopeKey: createReleaseCatalogScopeKey({
|
|
46
|
+
channelKey,
|
|
47
|
+
fingerprintHash,
|
|
48
|
+
platform,
|
|
49
|
+
strategy: "FINGERPRINT"
|
|
50
|
+
}),
|
|
51
|
+
strategy: "FINGERPRINT"
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
/** A release and its scope, checked against the revision the caller saw. */
|
|
55
|
+
const loadTarget = async (tx, { releaseId, expectedRevision }) => {
|
|
56
|
+
const row = await tx.findOne("releases", { id: releaseId });
|
|
57
|
+
if (row === null) throw new ReleaseManagementError("RELEASE_NOT_FOUND", `Release "${releaseId}" was not found.`);
|
|
58
|
+
const release = toReleaseRow(row);
|
|
59
|
+
if (expectedRevision !== void 0 && expectedRevision !== release.revision) throw new ReleaseManagementError("VERSION_CONFLICT", `Release "${release.id}" is revision ${release.revision}; expected ${expectedRevision}.`);
|
|
60
|
+
const catalog = await tx.findOne("release_catalogs", { scope_key: release.scope_key });
|
|
61
|
+
if (catalog === null) throw new ReleaseCatalogMutationError("INVALID_SCOPE", `Release "${release.id}" has no catalog projection.`);
|
|
62
|
+
return {
|
|
63
|
+
release,
|
|
64
|
+
scope: {
|
|
65
|
+
channelId: release.channel_id,
|
|
66
|
+
channelName: decodeChannelKey(catalog.channel_key),
|
|
67
|
+
fingerprintHash: release.fingerprint_hash,
|
|
68
|
+
platform: release.platform,
|
|
69
|
+
scopeKey: release.scope_key,
|
|
70
|
+
strategy: release.strategy
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
const policyUpdate = (release, patch, updatedAtMs) => {
|
|
75
|
+
if (patch.fingerprintHash !== void 0 && patch.fingerprintHash !== release.fingerprint_hash) throw new ReleaseManagementError("SCOPE_MOVE_UNSUPPORTED", "Changing a fingerprint target requires an atomic Release scope move.");
|
|
76
|
+
return {
|
|
77
|
+
...patch.enabled === void 0 ? {} : { enabled: patch.enabled },
|
|
78
|
+
...patch.message === void 0 ? {} : { message: patch.message },
|
|
79
|
+
...patch.rolloutCohortCount === void 0 ? {} : { rollout_cohort_count: patch.rolloutCohortCount },
|
|
80
|
+
...patch.shouldForceUpdate === void 0 ? {} : { should_force_update: patch.shouldForceUpdate },
|
|
81
|
+
...patch.targetAppVersion === void 0 ? {} : { target_app_version: patch.targetAppVersion },
|
|
82
|
+
...patch.targetCohorts === void 0 ? {} : { target_cohorts: [...patch.targetCohorts] },
|
|
83
|
+
updated_at_ms: updatedAtMs
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
const mutationResult = ({ catalog, release }) => ({
|
|
87
|
+
attempts: 1,
|
|
88
|
+
catalog,
|
|
89
|
+
release
|
|
90
|
+
});
|
|
91
|
+
/** The scope a catalog row names, for a rebuild. */
|
|
92
|
+
const catalogScope = async (tx, scopeKey) => {
|
|
93
|
+
const row = await tx.findOne("release_catalogs", { scope_key: scopeKey });
|
|
94
|
+
if (row === null) throw new Error(`Release catalog "${scopeKey}" was not found.`);
|
|
95
|
+
return {
|
|
96
|
+
channelId: row.channel_id,
|
|
97
|
+
channelName: decodeChannelKey(row.channel_key),
|
|
98
|
+
fingerprintHash: row.fingerprint_hash,
|
|
99
|
+
platform: row.platform,
|
|
100
|
+
scopeKey: row.scope_key,
|
|
101
|
+
strategy: row.strategy
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* Core's typed writes: each runs in one engine transaction rooted at the
|
|
106
|
+
* catalogs it changes, so a concurrent change reruns it instead of
|
|
107
|
+
* publishing a stale catalog.
|
|
108
|
+
*/
|
|
109
|
+
const createCoreOperations = (db, { now = Date.now } = {}) => {
|
|
110
|
+
const ensureChannel = async (name) => (await insertChannel(db, {
|
|
111
|
+
id: `channel:${encodeChannelKey(name)}`,
|
|
112
|
+
name
|
|
113
|
+
})).row;
|
|
114
|
+
/**
|
|
115
|
+
* Runs a write that ensured its channels first. A channel deleted between
|
|
116
|
+
* that and the write refuses the write, so it runs again and ensures it anew.
|
|
117
|
+
*/
|
|
118
|
+
const withChannels = async (write) => {
|
|
119
|
+
for (let attempt = 1;; attempt += 1) try {
|
|
120
|
+
return await write();
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (attempt < 3 && error instanceof DatabaseConstraintError && error.reason === "not_found" && error.model === "channels") continue;
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
const deployOnce = async (deployments) => {
|
|
127
|
+
const channels = /* @__PURE__ */ new Map();
|
|
128
|
+
for (const { release } of deployments) if (!channels.has(release.channel)) channels.set(release.channel, await ensureChannel(release.channel));
|
|
129
|
+
const platforms = /* @__PURE__ */ new Map();
|
|
130
|
+
for (const deployment of deployments) {
|
|
131
|
+
if (!("bundleId" in deployment) || platforms.has(deployment.bundleId)) continue;
|
|
132
|
+
const row = await db.findOne("bundles", { id: deployment.bundleId });
|
|
133
|
+
if (row === null) throw new DatabaseBundleNotFoundError(deployment.bundleId);
|
|
134
|
+
platforms.set(deployment.bundleId, toBundleRow(row).platform);
|
|
135
|
+
}
|
|
136
|
+
const updatedAtMs = now();
|
|
137
|
+
return (await changeReleases(db, deployments.map((deployment) => {
|
|
138
|
+
const { release: policy } = deployment;
|
|
139
|
+
const stored = "bundleId" in deployment;
|
|
140
|
+
const bundleId = stored ? deployment.bundleId : deployment.bundle.id;
|
|
141
|
+
const platform = stored ? platforms.get(deployment.bundleId) : deployment.bundle.platform;
|
|
142
|
+
const scope = scopeOf(channels.get(policy.channel), platform, policy.fingerprintHash);
|
|
143
|
+
return {
|
|
144
|
+
scope,
|
|
145
|
+
...stored ? {} : { bundle: {
|
|
146
|
+
row: bundleToRow(deployment.bundle),
|
|
147
|
+
patches: bundleToPatchRows(deployment.bundle)
|
|
148
|
+
} },
|
|
149
|
+
change: {
|
|
150
|
+
operation: "insert",
|
|
151
|
+
row: {
|
|
152
|
+
bundle_id: bundleId,
|
|
153
|
+
channel_id: scope.channelId,
|
|
154
|
+
created_at_ms: updatedAtMs,
|
|
155
|
+
enabled: policy.enabled,
|
|
156
|
+
fingerprint_hash: policy.fingerprintHash,
|
|
157
|
+
kind: "BUNDLE",
|
|
158
|
+
message: policy.message,
|
|
159
|
+
operation: "DEPLOY",
|
|
160
|
+
platform,
|
|
161
|
+
revision: 1,
|
|
162
|
+
rollout_cohort_count: policy.rolloutCohortCount ?? 1e3,
|
|
163
|
+
scope_key: scope.scopeKey,
|
|
164
|
+
should_force_update: policy.shouldForceUpdate,
|
|
165
|
+
source_release_id: null,
|
|
166
|
+
strategy: scope.strategy,
|
|
167
|
+
target_app_version: policy.targetAppVersion,
|
|
168
|
+
target_cohorts: policy.targetCohorts ?? [],
|
|
169
|
+
updated_at_ms: updatedAtMs
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
updatedAtMs
|
|
173
|
+
};
|
|
174
|
+
}))).map(mutationResult);
|
|
175
|
+
};
|
|
176
|
+
return {
|
|
177
|
+
deploy: (deployments) => withChannels(() => deployOnce(deployments)),
|
|
178
|
+
updateReleasePolicy: (input) => {
|
|
179
|
+
const updatedAtMs = now();
|
|
180
|
+
return db.transaction(async (tx) => {
|
|
181
|
+
const { release, scope } = await loadTarget(tx, input);
|
|
182
|
+
return mutationResult(await changeRelease(tx, {
|
|
183
|
+
scope,
|
|
184
|
+
change: {
|
|
185
|
+
operation: "update",
|
|
186
|
+
id: release.id,
|
|
187
|
+
update: policyUpdate(release, input.patch, updatedAtMs)
|
|
188
|
+
},
|
|
189
|
+
updatedAtMs
|
|
190
|
+
}));
|
|
191
|
+
});
|
|
192
|
+
},
|
|
193
|
+
preflightReleasePolicy: (input) => {
|
|
194
|
+
const updatedAtMs = now();
|
|
195
|
+
return dryRun(db, async (tx) => {
|
|
196
|
+
const { release, scope } = await loadTarget(tx, input);
|
|
197
|
+
const stored = await readCatalog(tx, scope.scopeKey);
|
|
198
|
+
const changed = await changeRelease(tx, {
|
|
199
|
+
scope,
|
|
200
|
+
change: {
|
|
201
|
+
operation: "update",
|
|
202
|
+
id: release.id,
|
|
203
|
+
update: policyUpdate(release, input.patch, updatedAtMs)
|
|
204
|
+
},
|
|
205
|
+
updatedAtMs
|
|
206
|
+
});
|
|
207
|
+
return {
|
|
208
|
+
catalog: changed.catalog,
|
|
209
|
+
currentCatalog: stored.catalog,
|
|
210
|
+
diagnostics: changed.diagnostics,
|
|
211
|
+
expectedReleaseRevision: release.revision,
|
|
212
|
+
release: changed.release
|
|
213
|
+
};
|
|
214
|
+
});
|
|
215
|
+
},
|
|
216
|
+
deleteRelease: (input) => db.transaction(async (tx) => {
|
|
217
|
+
const { release, scope } = await loadTarget(tx, input);
|
|
218
|
+
if (release.enabled) throw new ReleaseManagementError("ENABLED_RELEASE", `Disable Release "${release.id}" before hard deletion.`);
|
|
219
|
+
const bundle = release.bundle_id === null ? null : await tx.findOne("bundles", { id: release.bundle_id });
|
|
220
|
+
const result = mutationResult(await changeRelease(tx, {
|
|
221
|
+
scope,
|
|
222
|
+
change: {
|
|
223
|
+
operation: "delete",
|
|
224
|
+
id: release.id
|
|
225
|
+
},
|
|
226
|
+
updatedAtMs: now()
|
|
227
|
+
}));
|
|
228
|
+
if (bundle !== null && (bundle._refs_releases_bundle_id ?? 0) <= 1) await deleteBundle(tx, bundle);
|
|
229
|
+
return result;
|
|
230
|
+
}),
|
|
231
|
+
promoteRelease: (input) => withChannels(async () => {
|
|
232
|
+
const checkSource = (source) => {
|
|
233
|
+
if (source.kind !== "BUNDLE" || source.bundle_id === null) throw new ReleaseManagementError("TARGET_RELEASE_INVALID", "Only a Bundle Release can be promoted.");
|
|
234
|
+
};
|
|
235
|
+
checkSource((await db.transaction((tx) => loadTarget(tx, input))).release);
|
|
236
|
+
const targetChannel = input.targetChannel.trim();
|
|
237
|
+
if (targetChannel.length === 0) throw new ReleaseManagementError("TARGET_RELEASE_INVALID", "Promotion requires a target channel.");
|
|
238
|
+
const channel = await ensureChannel(targetChannel);
|
|
239
|
+
const updatedAtMs = now();
|
|
240
|
+
return db.transaction(async (tx) => {
|
|
241
|
+
const { release: source, scope: sourceScope } = await loadTarget(tx, input);
|
|
242
|
+
checkSource(source);
|
|
243
|
+
const targetScope = scopeOf(channel, source.platform, source.fingerprint_hash);
|
|
244
|
+
if (targetScope.scopeKey === sourceScope.scopeKey) throw new ReleaseManagementError("TARGET_RELEASE_INVALID", "Source and target Release scopes are the same.");
|
|
245
|
+
const { rows } = await tx.findMany("releases", {
|
|
246
|
+
index: "byScope",
|
|
247
|
+
where: { scope_key: targetScope.scopeKey },
|
|
248
|
+
order: "desc",
|
|
249
|
+
limit: 1
|
|
250
|
+
});
|
|
251
|
+
const floor = [
|
|
252
|
+
rows[0]?.id ?? null,
|
|
253
|
+
source.id,
|
|
254
|
+
source.bundle_id
|
|
255
|
+
].filter((id) => id !== null && isUUIDv7(id)).sort().at(-1);
|
|
256
|
+
const moved = input.action === "move" ? await changeRelease(tx, {
|
|
257
|
+
scope: sourceScope,
|
|
258
|
+
change: {
|
|
259
|
+
operation: "update",
|
|
260
|
+
id: source.id,
|
|
261
|
+
update: {
|
|
262
|
+
enabled: false,
|
|
263
|
+
updated_at_ms: updatedAtMs
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
updatedAtMs
|
|
267
|
+
}) : null;
|
|
268
|
+
const target = await changeRelease(tx, {
|
|
269
|
+
scope: targetScope,
|
|
270
|
+
change: {
|
|
271
|
+
operation: "insert",
|
|
272
|
+
row: {
|
|
273
|
+
...source,
|
|
274
|
+
id: createUUIDv7After(floor ?? null, updatedAtMs),
|
|
275
|
+
revision: 1,
|
|
276
|
+
scope_key: targetScope.scopeKey,
|
|
277
|
+
channel_id: targetScope.channelId,
|
|
278
|
+
enabled: true,
|
|
279
|
+
rollout_cohort_count: 1e3,
|
|
280
|
+
target_cohorts: [],
|
|
281
|
+
operation: "PROMOTE",
|
|
282
|
+
source_release_id: source.id,
|
|
283
|
+
created_at_ms: updatedAtMs,
|
|
284
|
+
updated_at_ms: updatedAtMs
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
updatedAtMs
|
|
288
|
+
});
|
|
289
|
+
return {
|
|
290
|
+
source: moved === null ? null : mutationResult(moved),
|
|
291
|
+
target: mutationResult(target)
|
|
292
|
+
};
|
|
293
|
+
});
|
|
294
|
+
}),
|
|
295
|
+
rebuildReleaseCatalog: (scopeKey) => {
|
|
296
|
+
const updatedAtMs = now();
|
|
297
|
+
return db.transaction(async (tx) => {
|
|
298
|
+
const { current: _current, ...rebuilt } = await rebuildScope(tx, await catalogScope(tx, scopeKey), updatedAtMs);
|
|
299
|
+
return {
|
|
300
|
+
attempts: 1,
|
|
301
|
+
...rebuilt
|
|
302
|
+
};
|
|
303
|
+
});
|
|
304
|
+
},
|
|
305
|
+
preflightReleaseCatalogRebuild: (scopeKey) => {
|
|
306
|
+
const updatedAtMs = now();
|
|
307
|
+
return dryRun(db, async (tx) => {
|
|
308
|
+
const rebuilt = await rebuildScope(tx, await catalogScope(tx, scopeKey), updatedAtMs);
|
|
309
|
+
return {
|
|
310
|
+
changed: rebuilt.changed,
|
|
311
|
+
currentCatalog: rebuilt.current,
|
|
312
|
+
diagnostics: rebuilt.diagnostics,
|
|
313
|
+
projectedCatalog: rebuilt.catalog
|
|
314
|
+
};
|
|
315
|
+
});
|
|
316
|
+
},
|
|
317
|
+
ensureChannel,
|
|
318
|
+
deleteChannel: (id) => deleteChannel(db, id),
|
|
319
|
+
updateBundle: (id, update) => db.transaction(async (tx) => {
|
|
320
|
+
const current = await tx.findOne("bundles", { id });
|
|
321
|
+
if (current === null) throw new DatabaseBundleNotFoundError(id);
|
|
322
|
+
const { patches, ...fields } = update;
|
|
323
|
+
const next = {
|
|
324
|
+
...rowToBundle(toBundleRow(current)),
|
|
325
|
+
...fields,
|
|
326
|
+
id
|
|
327
|
+
};
|
|
328
|
+
const { id: _id, ...set } = bundleToRow(next);
|
|
329
|
+
updateBundle(tx, current, set);
|
|
330
|
+
if (patches !== void 0) await replaceBundlePatches(tx, current, bundleToPatchRows({
|
|
331
|
+
...next,
|
|
332
|
+
patches
|
|
333
|
+
}));
|
|
334
|
+
}),
|
|
335
|
+
deleteBundles: async (ids) => {
|
|
336
|
+
try {
|
|
337
|
+
await db.transaction(async (tx) => {
|
|
338
|
+
for (const id of new Set(ids)) {
|
|
339
|
+
const current = await tx.findOne("bundles", { id });
|
|
340
|
+
if (current !== null) await deleteBundle(tx, current);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
} catch (error) {
|
|
344
|
+
if (error instanceof DatabaseConstraintError && error.reason === "referenced") throw new DatabaseRowReferencedError();
|
|
345
|
+
throw error;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
};
|
|
350
|
+
//#endregion
|
|
351
|
+
export { createCoreOperations };
|