@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,193 @@
|
|
|
1
|
+
const require_reads = require("./reads.cjs");
|
|
2
|
+
const require_writes = require("./writes.cjs");
|
|
3
|
+
let _hot_updater_plugin_core = require("@hot-updater/plugin-core");
|
|
4
|
+
let _hot_updater_protocol = require("@hot-updater/protocol");
|
|
5
|
+
//#region src/core/releases.ts
|
|
6
|
+
const PAGE = 500;
|
|
7
|
+
const fail = (code, message) => {
|
|
8
|
+
throw new _hot_updater_plugin_core.ReleaseCatalogMutationError(code, message);
|
|
9
|
+
};
|
|
10
|
+
const checkScope = (scope) => {
|
|
11
|
+
const channelKey = (0, _hot_updater_protocol.encodeChannelKey)(scope.channelName);
|
|
12
|
+
const expected = scope.strategy === "APP_VERSION" ? (0, _hot_updater_protocol.createReleaseCatalogScopeKey)({
|
|
13
|
+
channelKey,
|
|
14
|
+
platform: scope.platform,
|
|
15
|
+
strategy: "APP_VERSION"
|
|
16
|
+
}) : (0, _hot_updater_protocol.createReleaseCatalogScopeKey)({
|
|
17
|
+
channelKey,
|
|
18
|
+
fingerprintHash: scope.fingerprintHash ?? "",
|
|
19
|
+
platform: scope.platform,
|
|
20
|
+
strategy: "FINGERPRINT"
|
|
21
|
+
});
|
|
22
|
+
if (scope.strategy === "FINGERPRINT" !== (scope.fingerprintHash !== null) || scope.scopeKey !== expected) fail("INVALID_SCOPE", "Release catalog scope metadata is inconsistent");
|
|
23
|
+
};
|
|
24
|
+
/** A scope's enabled releases, all of them, under the catalog's guard. */
|
|
25
|
+
const enabledReleases = async (tx, scopeKey) => {
|
|
26
|
+
const rows = [];
|
|
27
|
+
let cursor;
|
|
28
|
+
do {
|
|
29
|
+
const page = await tx.findMany("releases", {
|
|
30
|
+
index: "byScopeEnabled",
|
|
31
|
+
where: {
|
|
32
|
+
scope_key: scopeKey,
|
|
33
|
+
enabled: true
|
|
34
|
+
},
|
|
35
|
+
limit: PAGE,
|
|
36
|
+
...cursor === void 0 ? {} : { cursor }
|
|
37
|
+
});
|
|
38
|
+
rows.push(...page.rows.map(require_reads.toReleaseRow));
|
|
39
|
+
cursor = page.next;
|
|
40
|
+
} while (cursor !== void 0);
|
|
41
|
+
return rows;
|
|
42
|
+
};
|
|
43
|
+
/** The scope's newest release id: `byScope` descending, limit 1. */
|
|
44
|
+
const latestReleaseId = async (tx, scopeKey) => {
|
|
45
|
+
const { rows } = await tx.findMany("releases", {
|
|
46
|
+
index: "byScope",
|
|
47
|
+
where: { scope_key: scopeKey },
|
|
48
|
+
order: "desc",
|
|
49
|
+
limit: 1
|
|
50
|
+
});
|
|
51
|
+
return rows[0]?.id ?? null;
|
|
52
|
+
};
|
|
53
|
+
/** Compiles `enabled` and writes it as the scope's next catalog generation. */
|
|
54
|
+
const writeCatalog = async (tx, scope, stored, enabled, updatedAtMs) => {
|
|
55
|
+
const compilation = await (0, _hot_updater_plugin_core.compileReleaseCatalog)({
|
|
56
|
+
strategy: scope.strategy,
|
|
57
|
+
releases: enabled.map(_hot_updater_plugin_core.releaseRowToRelease)
|
|
58
|
+
});
|
|
59
|
+
const generation = (stored.generation ?? 0) + 1;
|
|
60
|
+
if (!Number.isSafeInteger(generation)) fail("CATALOG_GENERATION_EXHAUSTED", `Catalog "${scope.scopeKey}" exhausted its generation counter`);
|
|
61
|
+
const catalog = {
|
|
62
|
+
scope_key: scope.scopeKey,
|
|
63
|
+
catalog_id: stored.catalog?.catalog_id ?? (0, _hot_updater_plugin_core.createUUIDv7)(),
|
|
64
|
+
strategy: scope.strategy,
|
|
65
|
+
channel_id: scope.channelId,
|
|
66
|
+
channel_key: (0, _hot_updater_protocol.encodeChannelKey)(scope.channelName),
|
|
67
|
+
platform: scope.platform,
|
|
68
|
+
fingerprint_hash: scope.fingerprintHash,
|
|
69
|
+
generation,
|
|
70
|
+
payload: compilation.canonicalPayload,
|
|
71
|
+
catalog_hash: compilation.catalogHash,
|
|
72
|
+
byte_size: compilation.byteSize,
|
|
73
|
+
is_tombstone: compilation.payload.releaseDescriptors.length === 0,
|
|
74
|
+
updated_at_ms: updatedAtMs
|
|
75
|
+
};
|
|
76
|
+
const { scope_key: _scopeKey, ...set } = catalog;
|
|
77
|
+
if (stored.row === null) tx.create("release_catalogs", catalog);
|
|
78
|
+
else tx.update("release_catalogs", stored.row, set);
|
|
79
|
+
return {
|
|
80
|
+
catalog,
|
|
81
|
+
compilation
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
/** The scope's catalog row, and its compiled catalog and generation if it has one. */
|
|
85
|
+
const readCatalog = async (tx, scopeKey) => {
|
|
86
|
+
const row = await tx.findOne("release_catalogs", { scope_key: scopeKey });
|
|
87
|
+
return {
|
|
88
|
+
row,
|
|
89
|
+
generation: row?.generation ?? null,
|
|
90
|
+
catalog: row === null ? null : require_reads.toCatalogRow(row)
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
/** A scope with releases and no compiled catalog lost its history. */
|
|
94
|
+
const assertHistory = (stored, hasReleases) => {
|
|
95
|
+
if (stored.catalog === null && hasReleases) fail("CATALOG_IDENTITY_MISSING", "Catalog history is missing. Restore its catalog row from backup before rebuilding or deploying.");
|
|
96
|
+
};
|
|
97
|
+
/** One release change and its scope's next catalog, inside `tx`. */
|
|
98
|
+
const changeRelease = async (tx, { scope, change, bundle, updatedAtMs }) => {
|
|
99
|
+
checkScope(scope);
|
|
100
|
+
const stored = await readCatalog(tx, scope.scopeKey);
|
|
101
|
+
const enabled = await enabledReleases(tx, scope.scopeKey);
|
|
102
|
+
if (bundle !== void 0) {
|
|
103
|
+
require_writes.insertBundle(tx, bundle.row);
|
|
104
|
+
for (const patch of bundle.patches) tx.create("bundle_patches", patch);
|
|
105
|
+
}
|
|
106
|
+
let before = null;
|
|
107
|
+
let release;
|
|
108
|
+
if (change.operation === "insert") {
|
|
109
|
+
const latest = await latestReleaseId(tx, scope.scopeKey);
|
|
110
|
+
assertHistory(stored, latest !== null);
|
|
111
|
+
const floor = [latest, bundle && (0, _hot_updater_plugin_core.isUUIDv7)(bundle.row.id) ? bundle.row.id : null].filter((id) => id !== null).sort().at(-1);
|
|
112
|
+
const id = change.row.id ?? (0, _hot_updater_plugin_core.createUUIDv7After)(floor ?? null);
|
|
113
|
+
if (latest !== null && id <= latest) fail("NON_MONOTONIC_RELEASE_ID", `Release "${id}" must sort after "${latest}"`);
|
|
114
|
+
release = {
|
|
115
|
+
...change.row,
|
|
116
|
+
id
|
|
117
|
+
};
|
|
118
|
+
if (release.scope_key !== scope.scopeKey || release.channel_id !== scope.channelId || release.platform !== scope.platform || release.strategy !== scope.strategy || release.fingerprint_hash !== scope.fingerprintHash || release.revision !== 1) fail("INVALID_SCOPE", "Inserted Release does not match its catalog scope");
|
|
119
|
+
tx.create("releases", release);
|
|
120
|
+
} else {
|
|
121
|
+
const row = await tx.findOne("releases", { id: change.id });
|
|
122
|
+
if (row === null || row.scope_key !== scope.scopeKey) return fail("RELEASE_NOT_FOUND", `Release "${change.id}" was not found in scope "${scope.scopeKey}"`);
|
|
123
|
+
assertHistory(stored, true);
|
|
124
|
+
before = require_reads.toReleaseRow(row);
|
|
125
|
+
if (change.operation === "delete") {
|
|
126
|
+
await tx.delete("releases", row);
|
|
127
|
+
release = null;
|
|
128
|
+
} else {
|
|
129
|
+
const set = {
|
|
130
|
+
...change.update,
|
|
131
|
+
revision: before.revision + 1
|
|
132
|
+
};
|
|
133
|
+
tx.update("releases", row, set);
|
|
134
|
+
release = {
|
|
135
|
+
...before,
|
|
136
|
+
...set
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const changedId = before?.id ?? release?.id;
|
|
141
|
+
const next = [...enabled.filter(({ id }) => id !== changedId), ...release?.enabled ? [release] : []].sort((left, right) => left.id < right.id ? -1 : 1);
|
|
142
|
+
const { catalog, compilation } = await writeCatalog(tx, scope, stored, next, updatedAtMs);
|
|
143
|
+
return {
|
|
144
|
+
catalog,
|
|
145
|
+
release,
|
|
146
|
+
diagnostics: compilation.diagnostics
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
/**
|
|
150
|
+
* Release changes rooted at their catalogs: each reads its scope's catalog
|
|
151
|
+
* and enabled releases, applies the change, and writes the next catalog
|
|
152
|
+
* generation, all in one transaction. A concurrent change in a scope bumps
|
|
153
|
+
* its catalog row, so this transaction reruns instead of publishing a stale
|
|
154
|
+
* catalog.
|
|
155
|
+
*/
|
|
156
|
+
const changeReleases = (db, inputs) => {
|
|
157
|
+
if (new Set(inputs.map(({ scope }) => scope.scopeKey)).size !== inputs.length) fail("INVALID_SCOPE", "A batch cannot change one catalog scope twice");
|
|
158
|
+
return db.transaction(async (tx) => {
|
|
159
|
+
const results = [];
|
|
160
|
+
for (const input of inputs) results.push(await changeRelease(tx, input));
|
|
161
|
+
return results;
|
|
162
|
+
});
|
|
163
|
+
};
|
|
164
|
+
/** Recompiles a scope's catalog from its enabled releases inside `tx`; an unchanged catalog is not rewritten. */
|
|
165
|
+
const rebuildScope = async (tx, scope, updatedAtMs) => {
|
|
166
|
+
checkScope(scope);
|
|
167
|
+
const stored = await readCatalog(tx, scope.scopeKey);
|
|
168
|
+
assertHistory(stored, await latestReleaseId(tx, scope.scopeKey) !== null);
|
|
169
|
+
const enabled = await enabledReleases(tx, scope.scopeKey);
|
|
170
|
+
const compilation = await (0, _hot_updater_plugin_core.compileReleaseCatalog)({
|
|
171
|
+
strategy: scope.strategy,
|
|
172
|
+
releases: enabled.map(_hot_updater_plugin_core.releaseRowToRelease)
|
|
173
|
+
});
|
|
174
|
+
const current = stored.catalog;
|
|
175
|
+
if (current !== null && current.strategy === scope.strategy && current.channel_id === scope.channelId && current.channel_key === (0, _hot_updater_protocol.encodeChannelKey)(scope.channelName) && current.platform === scope.platform && current.fingerprint_hash === scope.fingerprintHash && current.catalog_hash === compilation.catalogHash) return {
|
|
176
|
+
catalog: current,
|
|
177
|
+
current,
|
|
178
|
+
changed: false,
|
|
179
|
+
diagnostics: compilation.diagnostics
|
|
180
|
+
};
|
|
181
|
+
const written = await writeCatalog(tx, scope, stored, enabled, updatedAtMs);
|
|
182
|
+
return {
|
|
183
|
+
catalog: written.catalog,
|
|
184
|
+
current,
|
|
185
|
+
changed: true,
|
|
186
|
+
diagnostics: written.compilation.diagnostics
|
|
187
|
+
};
|
|
188
|
+
};
|
|
189
|
+
//#endregion
|
|
190
|
+
exports.changeRelease = changeRelease;
|
|
191
|
+
exports.changeReleases = changeReleases;
|
|
192
|
+
exports.readCatalog = readCatalog;
|
|
193
|
+
exports.rebuildScope = rebuildScope;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { toCatalogRow, toReleaseRow } from "./reads.mjs";
|
|
2
|
+
import { insertBundle } from "./writes.mjs";
|
|
3
|
+
import { ReleaseCatalogMutationError, compileReleaseCatalog, createUUIDv7, createUUIDv7After, isUUIDv7, releaseRowToRelease } from "@hot-updater/plugin-core";
|
|
4
|
+
import { createReleaseCatalogScopeKey, encodeChannelKey } from "@hot-updater/protocol";
|
|
5
|
+
//#region src/core/releases.ts
|
|
6
|
+
const PAGE = 500;
|
|
7
|
+
const fail = (code, message) => {
|
|
8
|
+
throw new ReleaseCatalogMutationError(code, message);
|
|
9
|
+
};
|
|
10
|
+
const checkScope = (scope) => {
|
|
11
|
+
const channelKey = encodeChannelKey(scope.channelName);
|
|
12
|
+
const expected = scope.strategy === "APP_VERSION" ? createReleaseCatalogScopeKey({
|
|
13
|
+
channelKey,
|
|
14
|
+
platform: scope.platform,
|
|
15
|
+
strategy: "APP_VERSION"
|
|
16
|
+
}) : createReleaseCatalogScopeKey({
|
|
17
|
+
channelKey,
|
|
18
|
+
fingerprintHash: scope.fingerprintHash ?? "",
|
|
19
|
+
platform: scope.platform,
|
|
20
|
+
strategy: "FINGERPRINT"
|
|
21
|
+
});
|
|
22
|
+
if (scope.strategy === "FINGERPRINT" !== (scope.fingerprintHash !== null) || scope.scopeKey !== expected) fail("INVALID_SCOPE", "Release catalog scope metadata is inconsistent");
|
|
23
|
+
};
|
|
24
|
+
/** A scope's enabled releases, all of them, under the catalog's guard. */
|
|
25
|
+
const enabledReleases = async (tx, scopeKey) => {
|
|
26
|
+
const rows = [];
|
|
27
|
+
let cursor;
|
|
28
|
+
do {
|
|
29
|
+
const page = await tx.findMany("releases", {
|
|
30
|
+
index: "byScopeEnabled",
|
|
31
|
+
where: {
|
|
32
|
+
scope_key: scopeKey,
|
|
33
|
+
enabled: true
|
|
34
|
+
},
|
|
35
|
+
limit: PAGE,
|
|
36
|
+
...cursor === void 0 ? {} : { cursor }
|
|
37
|
+
});
|
|
38
|
+
rows.push(...page.rows.map(toReleaseRow));
|
|
39
|
+
cursor = page.next;
|
|
40
|
+
} while (cursor !== void 0);
|
|
41
|
+
return rows;
|
|
42
|
+
};
|
|
43
|
+
/** The scope's newest release id: `byScope` descending, limit 1. */
|
|
44
|
+
const latestReleaseId = async (tx, scopeKey) => {
|
|
45
|
+
const { rows } = await tx.findMany("releases", {
|
|
46
|
+
index: "byScope",
|
|
47
|
+
where: { scope_key: scopeKey },
|
|
48
|
+
order: "desc",
|
|
49
|
+
limit: 1
|
|
50
|
+
});
|
|
51
|
+
return rows[0]?.id ?? null;
|
|
52
|
+
};
|
|
53
|
+
/** Compiles `enabled` and writes it as the scope's next catalog generation. */
|
|
54
|
+
const writeCatalog = async (tx, scope, stored, enabled, updatedAtMs) => {
|
|
55
|
+
const compilation = await compileReleaseCatalog({
|
|
56
|
+
strategy: scope.strategy,
|
|
57
|
+
releases: enabled.map(releaseRowToRelease)
|
|
58
|
+
});
|
|
59
|
+
const generation = (stored.generation ?? 0) + 1;
|
|
60
|
+
if (!Number.isSafeInteger(generation)) fail("CATALOG_GENERATION_EXHAUSTED", `Catalog "${scope.scopeKey}" exhausted its generation counter`);
|
|
61
|
+
const catalog = {
|
|
62
|
+
scope_key: scope.scopeKey,
|
|
63
|
+
catalog_id: stored.catalog?.catalog_id ?? createUUIDv7(),
|
|
64
|
+
strategy: scope.strategy,
|
|
65
|
+
channel_id: scope.channelId,
|
|
66
|
+
channel_key: encodeChannelKey(scope.channelName),
|
|
67
|
+
platform: scope.platform,
|
|
68
|
+
fingerprint_hash: scope.fingerprintHash,
|
|
69
|
+
generation,
|
|
70
|
+
payload: compilation.canonicalPayload,
|
|
71
|
+
catalog_hash: compilation.catalogHash,
|
|
72
|
+
byte_size: compilation.byteSize,
|
|
73
|
+
is_tombstone: compilation.payload.releaseDescriptors.length === 0,
|
|
74
|
+
updated_at_ms: updatedAtMs
|
|
75
|
+
};
|
|
76
|
+
const { scope_key: _scopeKey, ...set } = catalog;
|
|
77
|
+
if (stored.row === null) tx.create("release_catalogs", catalog);
|
|
78
|
+
else tx.update("release_catalogs", stored.row, set);
|
|
79
|
+
return {
|
|
80
|
+
catalog,
|
|
81
|
+
compilation
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
/** The scope's catalog row, and its compiled catalog and generation if it has one. */
|
|
85
|
+
const readCatalog = async (tx, scopeKey) => {
|
|
86
|
+
const row = await tx.findOne("release_catalogs", { scope_key: scopeKey });
|
|
87
|
+
return {
|
|
88
|
+
row,
|
|
89
|
+
generation: row?.generation ?? null,
|
|
90
|
+
catalog: row === null ? null : toCatalogRow(row)
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
/** A scope with releases and no compiled catalog lost its history. */
|
|
94
|
+
const assertHistory = (stored, hasReleases) => {
|
|
95
|
+
if (stored.catalog === null && hasReleases) fail("CATALOG_IDENTITY_MISSING", "Catalog history is missing. Restore its catalog row from backup before rebuilding or deploying.");
|
|
96
|
+
};
|
|
97
|
+
/** One release change and its scope's next catalog, inside `tx`. */
|
|
98
|
+
const changeRelease = async (tx, { scope, change, bundle, updatedAtMs }) => {
|
|
99
|
+
checkScope(scope);
|
|
100
|
+
const stored = await readCatalog(tx, scope.scopeKey);
|
|
101
|
+
const enabled = await enabledReleases(tx, scope.scopeKey);
|
|
102
|
+
if (bundle !== void 0) {
|
|
103
|
+
insertBundle(tx, bundle.row);
|
|
104
|
+
for (const patch of bundle.patches) tx.create("bundle_patches", patch);
|
|
105
|
+
}
|
|
106
|
+
let before = null;
|
|
107
|
+
let release;
|
|
108
|
+
if (change.operation === "insert") {
|
|
109
|
+
const latest = await latestReleaseId(tx, scope.scopeKey);
|
|
110
|
+
assertHistory(stored, latest !== null);
|
|
111
|
+
const floor = [latest, bundle && isUUIDv7(bundle.row.id) ? bundle.row.id : null].filter((id) => id !== null).sort().at(-1);
|
|
112
|
+
const id = change.row.id ?? createUUIDv7After(floor ?? null);
|
|
113
|
+
if (latest !== null && id <= latest) fail("NON_MONOTONIC_RELEASE_ID", `Release "${id}" must sort after "${latest}"`);
|
|
114
|
+
release = {
|
|
115
|
+
...change.row,
|
|
116
|
+
id
|
|
117
|
+
};
|
|
118
|
+
if (release.scope_key !== scope.scopeKey || release.channel_id !== scope.channelId || release.platform !== scope.platform || release.strategy !== scope.strategy || release.fingerprint_hash !== scope.fingerprintHash || release.revision !== 1) fail("INVALID_SCOPE", "Inserted Release does not match its catalog scope");
|
|
119
|
+
tx.create("releases", release);
|
|
120
|
+
} else {
|
|
121
|
+
const row = await tx.findOne("releases", { id: change.id });
|
|
122
|
+
if (row === null || row.scope_key !== scope.scopeKey) return fail("RELEASE_NOT_FOUND", `Release "${change.id}" was not found in scope "${scope.scopeKey}"`);
|
|
123
|
+
assertHistory(stored, true);
|
|
124
|
+
before = toReleaseRow(row);
|
|
125
|
+
if (change.operation === "delete") {
|
|
126
|
+
await tx.delete("releases", row);
|
|
127
|
+
release = null;
|
|
128
|
+
} else {
|
|
129
|
+
const set = {
|
|
130
|
+
...change.update,
|
|
131
|
+
revision: before.revision + 1
|
|
132
|
+
};
|
|
133
|
+
tx.update("releases", row, set);
|
|
134
|
+
release = {
|
|
135
|
+
...before,
|
|
136
|
+
...set
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const changedId = before?.id ?? release?.id;
|
|
141
|
+
const next = [...enabled.filter(({ id }) => id !== changedId), ...release?.enabled ? [release] : []].sort((left, right) => left.id < right.id ? -1 : 1);
|
|
142
|
+
const { catalog, compilation } = await writeCatalog(tx, scope, stored, next, updatedAtMs);
|
|
143
|
+
return {
|
|
144
|
+
catalog,
|
|
145
|
+
release,
|
|
146
|
+
diagnostics: compilation.diagnostics
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
/**
|
|
150
|
+
* Release changes rooted at their catalogs: each reads its scope's catalog
|
|
151
|
+
* and enabled releases, applies the change, and writes the next catalog
|
|
152
|
+
* generation, all in one transaction. A concurrent change in a scope bumps
|
|
153
|
+
* its catalog row, so this transaction reruns instead of publishing a stale
|
|
154
|
+
* catalog.
|
|
155
|
+
*/
|
|
156
|
+
const changeReleases = (db, inputs) => {
|
|
157
|
+
if (new Set(inputs.map(({ scope }) => scope.scopeKey)).size !== inputs.length) fail("INVALID_SCOPE", "A batch cannot change one catalog scope twice");
|
|
158
|
+
return db.transaction(async (tx) => {
|
|
159
|
+
const results = [];
|
|
160
|
+
for (const input of inputs) results.push(await changeRelease(tx, input));
|
|
161
|
+
return results;
|
|
162
|
+
});
|
|
163
|
+
};
|
|
164
|
+
/** Recompiles a scope's catalog from its enabled releases inside `tx`; an unchanged catalog is not rewritten. */
|
|
165
|
+
const rebuildScope = async (tx, scope, updatedAtMs) => {
|
|
166
|
+
checkScope(scope);
|
|
167
|
+
const stored = await readCatalog(tx, scope.scopeKey);
|
|
168
|
+
assertHistory(stored, await latestReleaseId(tx, scope.scopeKey) !== null);
|
|
169
|
+
const enabled = await enabledReleases(tx, scope.scopeKey);
|
|
170
|
+
const compilation = await compileReleaseCatalog({
|
|
171
|
+
strategy: scope.strategy,
|
|
172
|
+
releases: enabled.map(releaseRowToRelease)
|
|
173
|
+
});
|
|
174
|
+
const current = stored.catalog;
|
|
175
|
+
if (current !== null && current.strategy === scope.strategy && current.channel_id === scope.channelId && current.channel_key === encodeChannelKey(scope.channelName) && current.platform === scope.platform && current.fingerprint_hash === scope.fingerprintHash && current.catalog_hash === compilation.catalogHash) return {
|
|
176
|
+
catalog: current,
|
|
177
|
+
current,
|
|
178
|
+
changed: false,
|
|
179
|
+
diagnostics: compilation.diagnostics
|
|
180
|
+
};
|
|
181
|
+
const written = await writeCatalog(tx, scope, stored, enabled, updatedAtMs);
|
|
182
|
+
return {
|
|
183
|
+
catalog: written.catalog,
|
|
184
|
+
current,
|
|
185
|
+
changed: true,
|
|
186
|
+
diagnostics: written.compilation.diagnostics
|
|
187
|
+
};
|
|
188
|
+
};
|
|
189
|
+
//#endregion
|
|
190
|
+
export { changeRelease, changeReleases, readCatalog, rebuildScope };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
const require_reads = require("./reads.cjs");
|
|
2
|
+
let _hot_updater_plugin_core = require("@hot-updater/plugin-core");
|
|
3
|
+
//#region src/core/writes.ts
|
|
4
|
+
const PAGE = 500;
|
|
5
|
+
/** Moves one bundle into or out of its platform's total and the overall total. */
|
|
6
|
+
const countBundle = (tx, platform, delta) => {
|
|
7
|
+
for (const key of [platform, "*"]) tx.aggregate("bundle_totals", { platform_key: key }, { bundles: delta });
|
|
8
|
+
};
|
|
9
|
+
const insertBundle = (tx, row) => {
|
|
10
|
+
tx.create("bundles", row);
|
|
11
|
+
countBundle(tx, row.platform, 1);
|
|
12
|
+
};
|
|
13
|
+
const updateBundle = (tx, current, set) => {
|
|
14
|
+
tx.update("bundles", current, set);
|
|
15
|
+
if (set.platform !== void 0 && set.platform !== current.platform) {
|
|
16
|
+
countBundle(tx, current.platform, -1);
|
|
17
|
+
countBundle(tx, set.platform, 1);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
/** Deletes a bundle and, by cascade, every patch to or from it; a release on it refuses. */
|
|
21
|
+
const deleteBundle = async (tx, current) => {
|
|
22
|
+
await tx.delete("bundles", current);
|
|
23
|
+
countBundle(tx, current.platform, -1);
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Replaces a bundle's patches with `rows`: kept patches are updated where
|
|
27
|
+
* they changed, others deleted or created, so no key is written twice.
|
|
28
|
+
*/
|
|
29
|
+
const replaceBundlePatches = async (tx, bundle, rows) => {
|
|
30
|
+
const wanted = new Map(rows.map((row) => [row.id, row]));
|
|
31
|
+
const count = bundle._refs_bundle_patches_bundle_id ?? 0;
|
|
32
|
+
let cursor;
|
|
33
|
+
for (let seen = 0; seen < count;) {
|
|
34
|
+
const page = await tx.findMany("bundle_patches", {
|
|
35
|
+
index: "byBundle",
|
|
36
|
+
where: { bundle_id: bundle.id },
|
|
37
|
+
limit: Math.min(count - seen, PAGE),
|
|
38
|
+
...cursor === void 0 ? {} : { cursor }
|
|
39
|
+
});
|
|
40
|
+
for (const stored of page.rows) {
|
|
41
|
+
const next = wanted.get(stored.id);
|
|
42
|
+
wanted.delete(stored.id);
|
|
43
|
+
if (next === void 0) {
|
|
44
|
+
await tx.delete("bundle_patches", stored);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const set = Object.fromEntries(Object.entries(next).filter(([field, value]) => field !== "id" && stored[field] !== value));
|
|
48
|
+
if (Object.keys(set).length > 0) tx.update("bundle_patches", stored, set);
|
|
49
|
+
}
|
|
50
|
+
seen += page.rows.length;
|
|
51
|
+
if (page.next === void 0) break;
|
|
52
|
+
cursor = page.next;
|
|
53
|
+
}
|
|
54
|
+
for (const row of wanted.values()) tx.create("bundle_patches", row);
|
|
55
|
+
};
|
|
56
|
+
/** A channel by name, created when missing; a concurrent creator's row is returned instead. */
|
|
57
|
+
const insertChannel = async (db, row) => {
|
|
58
|
+
for (let attempt = 1;; attempt += 1) try {
|
|
59
|
+
return await db.transaction(async (tx) => {
|
|
60
|
+
const existing = await tx.findOne("channels", { name: row.name });
|
|
61
|
+
if (existing !== null) return {
|
|
62
|
+
row: require_reads.toChannelRow(existing),
|
|
63
|
+
inserted: false
|
|
64
|
+
};
|
|
65
|
+
tx.create("channels", row);
|
|
66
|
+
return {
|
|
67
|
+
row,
|
|
68
|
+
inserted: true
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (attempt < 3 && error instanceof _hot_updater_plugin_core.DatabaseConstraintError && (error.reason === "unique" || error.reason === "exists")) continue;
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
/** Deletes a channel no release uses: 1 read + 1 write. */
|
|
77
|
+
const deleteChannel = (db, id) => db.transaction(async (tx) => {
|
|
78
|
+
const row = await tx.findOne("channels", { id });
|
|
79
|
+
if (row === null) return {
|
|
80
|
+
deleted: false,
|
|
81
|
+
reason: "not_found"
|
|
82
|
+
};
|
|
83
|
+
if ((row._refs_releases_channel_id ?? 0) > 0) return {
|
|
84
|
+
deleted: false,
|
|
85
|
+
reason: "not_empty"
|
|
86
|
+
};
|
|
87
|
+
await tx.delete("channels", row);
|
|
88
|
+
return { deleted: true };
|
|
89
|
+
});
|
|
90
|
+
//#endregion
|
|
91
|
+
exports.deleteBundle = deleteBundle;
|
|
92
|
+
exports.deleteChannel = deleteChannel;
|
|
93
|
+
exports.insertBundle = insertBundle;
|
|
94
|
+
exports.insertChannel = insertChannel;
|
|
95
|
+
exports.replaceBundlePatches = replaceBundlePatches;
|
|
96
|
+
exports.updateBundle = updateBundle;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { toChannelRow } from "./reads.mjs";
|
|
2
|
+
import { DatabaseConstraintError } from "@hot-updater/plugin-core";
|
|
3
|
+
//#region src/core/writes.ts
|
|
4
|
+
const PAGE = 500;
|
|
5
|
+
/** Moves one bundle into or out of its platform's total and the overall total. */
|
|
6
|
+
const countBundle = (tx, platform, delta) => {
|
|
7
|
+
for (const key of [platform, "*"]) tx.aggregate("bundle_totals", { platform_key: key }, { bundles: delta });
|
|
8
|
+
};
|
|
9
|
+
const insertBundle = (tx, row) => {
|
|
10
|
+
tx.create("bundles", row);
|
|
11
|
+
countBundle(tx, row.platform, 1);
|
|
12
|
+
};
|
|
13
|
+
const updateBundle = (tx, current, set) => {
|
|
14
|
+
tx.update("bundles", current, set);
|
|
15
|
+
if (set.platform !== void 0 && set.platform !== current.platform) {
|
|
16
|
+
countBundle(tx, current.platform, -1);
|
|
17
|
+
countBundle(tx, set.platform, 1);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
/** Deletes a bundle and, by cascade, every patch to or from it; a release on it refuses. */
|
|
21
|
+
const deleteBundle = async (tx, current) => {
|
|
22
|
+
await tx.delete("bundles", current);
|
|
23
|
+
countBundle(tx, current.platform, -1);
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Replaces a bundle's patches with `rows`: kept patches are updated where
|
|
27
|
+
* they changed, others deleted or created, so no key is written twice.
|
|
28
|
+
*/
|
|
29
|
+
const replaceBundlePatches = async (tx, bundle, rows) => {
|
|
30
|
+
const wanted = new Map(rows.map((row) => [row.id, row]));
|
|
31
|
+
const count = bundle._refs_bundle_patches_bundle_id ?? 0;
|
|
32
|
+
let cursor;
|
|
33
|
+
for (let seen = 0; seen < count;) {
|
|
34
|
+
const page = await tx.findMany("bundle_patches", {
|
|
35
|
+
index: "byBundle",
|
|
36
|
+
where: { bundle_id: bundle.id },
|
|
37
|
+
limit: Math.min(count - seen, PAGE),
|
|
38
|
+
...cursor === void 0 ? {} : { cursor }
|
|
39
|
+
});
|
|
40
|
+
for (const stored of page.rows) {
|
|
41
|
+
const next = wanted.get(stored.id);
|
|
42
|
+
wanted.delete(stored.id);
|
|
43
|
+
if (next === void 0) {
|
|
44
|
+
await tx.delete("bundle_patches", stored);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const set = Object.fromEntries(Object.entries(next).filter(([field, value]) => field !== "id" && stored[field] !== value));
|
|
48
|
+
if (Object.keys(set).length > 0) tx.update("bundle_patches", stored, set);
|
|
49
|
+
}
|
|
50
|
+
seen += page.rows.length;
|
|
51
|
+
if (page.next === void 0) break;
|
|
52
|
+
cursor = page.next;
|
|
53
|
+
}
|
|
54
|
+
for (const row of wanted.values()) tx.create("bundle_patches", row);
|
|
55
|
+
};
|
|
56
|
+
/** A channel by name, created when missing; a concurrent creator's row is returned instead. */
|
|
57
|
+
const insertChannel = async (db, row) => {
|
|
58
|
+
for (let attempt = 1;; attempt += 1) try {
|
|
59
|
+
return await db.transaction(async (tx) => {
|
|
60
|
+
const existing = await tx.findOne("channels", { name: row.name });
|
|
61
|
+
if (existing !== null) return {
|
|
62
|
+
row: toChannelRow(existing),
|
|
63
|
+
inserted: false
|
|
64
|
+
};
|
|
65
|
+
tx.create("channels", row);
|
|
66
|
+
return {
|
|
67
|
+
row,
|
|
68
|
+
inserted: true
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (attempt < 3 && error instanceof DatabaseConstraintError && (error.reason === "unique" || error.reason === "exists")) continue;
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
/** Deletes a channel no release uses: 1 read + 1 write. */
|
|
77
|
+
const deleteChannel = (db, id) => db.transaction(async (tx) => {
|
|
78
|
+
const row = await tx.findOne("channels", { id });
|
|
79
|
+
if (row === null) return {
|
|
80
|
+
deleted: false,
|
|
81
|
+
reason: "not_found"
|
|
82
|
+
};
|
|
83
|
+
if ((row._refs_releases_channel_id ?? 0) > 0) return {
|
|
84
|
+
deleted: false,
|
|
85
|
+
reason: "not_empty"
|
|
86
|
+
};
|
|
87
|
+
await tx.delete("channels", row);
|
|
88
|
+
return { deleted: true };
|
|
89
|
+
});
|
|
90
|
+
//#endregion
|
|
91
|
+
export { deleteBundle, deleteChannel, insertBundle, insertChannel, replaceBundlePatches, updateBundle };
|