@hot-updater/aws 1.0.0-rc.14 → 1.0.0-rc.17

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.
@@ -23,7 +23,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
23
  }) : target, mod));
24
24
  //#endregion
25
25
  let _hot_updater_cli_tools = require("@hot-updater/cli-tools");
26
- let _hot_updater_server = require("@hot-updater/server");
26
+ let _hot_updater_server_db = require("@hot-updater/server/db");
27
27
  let node_url = require("node:url");
28
28
  let node_child_process = require("node:child_process");
29
29
  let node_string_decoder = require("node:string_decoder");
@@ -44,16 +44,17 @@ let node_fs = require("node:fs");
44
44
  let node_stream_promises = require("node:stream/promises");
45
45
  let node_stream = require("node:stream");
46
46
  let node_buffer = require("node:buffer");
47
- let node_crypto = require("node:crypto");
48
- let _aws_sdk_client_cloudfront = require("@aws-sdk/client-cloudfront");
49
47
  let _aws_sdk_client_dynamodb = require("@aws-sdk/client-dynamodb");
48
+ let _hot_updater_server_database = require("@hot-updater/server/database");
49
+ let _aws_sdk_client_cloudfront = require("@aws-sdk/client-cloudfront");
50
50
  let _aws_sdk_lib_dynamodb = require("@aws-sdk/lib-dynamodb");
51
- let _hot_updater_plugin_core = require("@hot-updater/plugin-core");
52
- let _hot_updater_plugin_core_internal = require("@hot-updater/plugin-core/internal");
51
+ let _hot_updater_server_plugins_api_keys = require("@hot-updater/server/plugins/api-keys");
52
+ let _hot_updater_server_plugins_insights = require("@hot-updater/server/plugins/insights");
53
53
  let _aws_sdk_credential_providers = require("@aws-sdk/credential-providers");
54
54
  let _aws_sdk_client_lambda = require("@aws-sdk/client-lambda");
55
- let crypto = require("crypto");
56
- crypto = __toESM(crypto, 1);
55
+ let crypto$1 = require("crypto");
56
+ crypto$1 = __toESM(crypto$1, 1);
57
+ let node_crypto = require("node:crypto");
57
58
  let _aws_sdk_client_iam = require("@aws-sdk/client-iam");
58
59
  let _aws_sdk_client_sts = require("@aws-sdk/client-sts");
59
60
  let fs_promises = require("fs/promises");
@@ -6276,7 +6277,7 @@ const { sendMessage, getOneMessage, getEachMessage, getCancelSignal } = getIpcEx
6276
6277
  //#region src/cloudFrontInvalidation.ts
6277
6278
  const DEFAULT_POLL_INTERVAL_MS = 2e3;
6278
6279
  const DEFAULT_TIMEOUT_MS = 3e5;
6279
- const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
6280
+ const sleep$1 = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
6280
6281
  const invalidateCloudFront = async (client, distributionId, paths, options) => {
6281
6282
  if (paths.length === 0) return;
6282
6283
  const response = await client.send(new _aws_sdk_client_cloudfront.CreateInvalidationCommand({
@@ -6294,7 +6295,7 @@ const invalidateCloudFront = async (client, distributionId, paths, options) => {
6294
6295
  if (!invalidationId) throw new Error("CloudFront invalidation response is missing Invalidation.Id");
6295
6296
  const deadline = Date.now() + DEFAULT_TIMEOUT_MS;
6296
6297
  while (Date.now() < deadline) {
6297
- await sleep(DEFAULT_POLL_INTERVAL_MS);
6298
+ await sleep$1(DEFAULT_POLL_INTERVAL_MS);
6298
6299
  if ((await client.send(new _aws_sdk_client_cloudfront.GetInvalidationCommand({
6299
6300
  DistributionId: distributionId,
6300
6301
  Id: invalidationId
@@ -6302,2001 +6303,294 @@ const invalidateCloudFront = async (client, distributionId, paths, options) => {
6302
6303
  }
6303
6304
  throw new Error(`Timed out waiting for CloudFront invalidation ${invalidationId}.`);
6304
6305
  };
6305
- //#endregion
6306
- //#region src/dynamoDB.ts
6307
- const DYNAMODB_MAX_METADATA_ITEM_BYTES = 8192;
6308
- const DYNAMODB_MAX_CATALOG_ITEM_BYTES = 409600;
6309
- var DynamoDBMetadataItemSizeError = class extends Error {
6310
- byteLength;
6311
- name = "DynamoDBMetadataItemSizeError";
6312
- constructor(byteLength) {
6313
- super(`DynamoDB metadata item is ${byteLength} bytes; maximum is ${DYNAMODB_MAX_METADATA_ITEM_BYTES}`);
6314
- this.byteLength = byteLength;
6315
- }
6316
- };
6317
- const boundedDynamoDBMetadataItem = (item) => {
6318
- const byteLength = new TextEncoder().encode(JSON.stringify(item)).byteLength;
6319
- if (byteLength > 8192) throw new DynamoDBMetadataItemSizeError(byteLength);
6320
- return item;
6321
- };
6322
- var DynamoDBCatalogItemSizeError = class extends Error {
6323
- byteLength;
6324
- name = "DynamoDBCatalogItemSizeError";
6325
- constructor(byteLength) {
6326
- super(`DynamoDB catalog item is ${byteLength} bytes; maximum is ${DYNAMODB_MAX_CATALOG_ITEM_BYTES}`);
6327
- this.byteLength = byteLength;
6328
- }
6329
- };
6330
- const boundedDynamoDBCatalogItem = (item) => {
6331
- const byteLength = new TextEncoder().encode(JSON.stringify(item)).byteLength;
6332
- if (byteLength > 409600) throw new DynamoDBCatalogItemSizeError(byteLength);
6333
- return item;
6334
- };
6335
- const DYNAMODB_CHANNEL_PARTITION = "channels";
6336
- const DYNAMODB_CHANNEL_NAME_PARTITION = "_hot-updater#channel-names";
6337
- var DynamoDBStoredItemError = class extends Error {
6338
- name = "DynamoDBStoredItemError";
6339
- constructor() {
6340
- super("DynamoDB contains an invalid Hot Updater row");
6341
- }
6342
- };
6343
- const field = (value, name) => Reflect.get(value, name);
6344
- const isNullableString = (value) => value === null || typeof value === "string";
6345
- const isByteSize = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
6346
- const withLegacyArchiveByteSize = (value) => typeof value === "object" && value !== null && field(value, "archive_byte_size") === void 0 ? {
6347
- ...value,
6348
- archive_byte_size: 0
6349
- } : value;
6350
- const isBundleRow = (value) => typeof value === "object" && value !== null && typeof field(value, "id") === "string" && (field(value, "platform") === "ios" || field(value, "platform") === "android") && typeof field(value, "file_hash") === "string" && isNullableString(field(value, "git_commit_hash")) && typeof field(value, "storage_uri") === "string" && isByteSize(field(value, "archive_byte_size")) && (0, _hot_updater_plugin_core.isDatabaseMetadataObject)(field(value, "metadata")) && isNullableString(field(value, "manifest_storage_uri")) && isNullableString(field(value, "manifest_file_hash")) && isNullableString(field(value, "asset_base_storage_uri"));
6351
- const isPatchRow = (value) => typeof value === "object" && value !== null && typeof field(value, "id") === "string" && typeof field(value, "bundle_id") === "string" && typeof field(value, "base_bundle_id") === "string" && typeof field(value, "base_file_hash") === "string" && typeof field(value, "patch_file_hash") === "string" && typeof field(value, "patch_storage_uri") === "string" && isByteSize(field(value, "byte_size")) && typeof field(value, "order_index") === "number";
6352
- const isChannelRow = (value) => typeof value === "object" && value !== null && typeof field(value, "id") === "string" && typeof field(value, "name") === "string";
6353
- const isReleaseRow = (value) => typeof value === "object" && value !== null && typeof field(value, "id") === "string" && typeof field(value, "revision") === "number" && typeof field(value, "scope_key") === "string" && typeof field(value, "channel_id") === "string" && (field(value, "platform") === "ios" || field(value, "platform") === "android") && (field(value, "kind") === "BUNDLE" || field(value, "kind") === "EMBEDDED") && isNullableString(field(value, "bundle_id")) && (field(value, "strategy") === "APP_VERSION" || field(value, "strategy") === "FINGERPRINT") && isNullableString(field(value, "target_app_version")) && isNullableString(field(value, "fingerprint_hash")) && typeof field(value, "enabled") === "boolean" && typeof field(value, "should_force_update") === "boolean" && isNullableString(field(value, "message")) && typeof field(value, "rollout_cohort_count") === "number" && Array.isArray(field(value, "target_cohorts")) && field(value, "target_cohorts").every((cohort) => typeof cohort === "string") && (field(value, "operation") === "DEPLOY" || field(value, "operation") === "PROMOTE" || field(value, "operation") === "ROLLBACK") && isNullableString(field(value, "source_release_id")) && typeof field(value, "created_at_ms") === "number" && typeof field(value, "updated_at_ms") === "number";
6354
- const isReleaseCatalogRow = (value) => typeof value === "object" && value !== null && typeof field(value, "scope_key") === "string" && typeof field(value, "catalog_id") === "string" && (field(value, "strategy") === "APP_VERSION" || field(value, "strategy") === "FINGERPRINT") && typeof field(value, "channel_id") === "string" && typeof field(value, "channel_key") === "string" && (field(value, "platform") === "ios" || field(value, "platform") === "android") && isNullableString(field(value, "fingerprint_hash")) && typeof field(value, "generation") === "number" && typeof field(value, "payload") === "string" && typeof field(value, "catalog_hash") === "string" && typeof field(value, "byte_size") === "number" && typeof field(value, "is_tombstone") === "boolean" && typeof field(value, "updated_at_ms") === "number";
6355
- const parseDynamoDBItem = (value) => {
6356
- const pk = value.pk;
6357
- const sk = value.sk;
6358
- const version = value.version;
6359
- const relationCount = value.relation_count;
6360
- const ownedPatchCount = value.owned_patch_count;
6361
- const gsi1pk = value.gsi1pk;
6362
- const gsi1sk = value.gsi1sk;
6363
- const row = pk === "bundles" ? withLegacyArchiveByteSize(value.row) : value.row;
6364
- if (typeof sk !== "string" || typeof version !== "number") throw new DynamoDBStoredItemError();
6365
- if (pk === "channels" && typeof value.reference_count === "number" && isChannelRow(row)) return {
6366
- pk,
6367
- sk,
6368
- version,
6369
- reference_count: value.reference_count,
6370
- row
6371
- };
6372
- if (typeof pk === "string" && pk.startsWith("release-scope#") && isReleaseRow(row)) return {
6373
- pk,
6374
- sk,
6375
- version,
6376
- row
6377
- };
6378
- if (pk === "release_catalogs" && isReleaseCatalogRow(row)) return {
6379
- pk,
6380
- sk,
6381
- version,
6382
- row
6383
- };
6384
- if (pk === "bundles" && isBundleRow(row)) {
6385
- if (typeof relationCount !== "number" || typeof ownedPatchCount !== "number") throw new DynamoDBStoredItemError();
6386
- return {
6387
- pk,
6388
- sk,
6389
- version,
6390
- relation_count: relationCount,
6391
- owned_patch_count: ownedPatchCount,
6392
- row
6393
- };
6394
- }
6395
- if (pk === "bundle_patches" && typeof gsi1pk === "string" && typeof gsi1sk === "string" && isPatchRow(row)) return {
6396
- pk,
6397
- sk,
6398
- version,
6399
- gsi1pk,
6400
- gsi1sk,
6401
- row
6402
- };
6403
- throw new DynamoDBStoredItemError();
6404
- };
6405
- const patchOwnerPartition = (bundleId) => `patch-owner#${bundleId}`;
6406
- const patchOrderKey = (row) => `${row.order_index.toString().padStart(10, "0")}#${row.id}`;
6407
- const toDynamoDBBundleItem = (row, version = 1, relationCount = 0, ownedPatchCount = 0) => ({
6408
- pk: "bundles",
6409
- sk: row.id,
6410
- version,
6411
- relation_count: relationCount,
6412
- owned_patch_count: ownedPatchCount,
6413
- row
6414
- });
6415
- const toDynamoDBPatchItem = (row, version = 1) => {
6416
- const orderKey = patchOrderKey(row);
6306
+ /**
6307
+ * The update-check routes' CloudFront invalidation: `invalidate` purges their
6308
+ * cached copies and `destroy` releases the client; undefined without a
6309
+ * distribution. A failed invalidation only warns, so the write that changed
6310
+ * the routes still succeeds.
6311
+ */
6312
+ const createUpdateRouteInvalidation = ({ apiBasePath = "/release-catalogs", cloudfrontDistributionId, shouldWaitForInvalidation = false, credentials, region }) => {
6313
+ if (!cloudfrontDistributionId) return void 0;
6314
+ const client = new _aws_sdk_client_cloudfront.CloudFrontClient({
6315
+ credentials,
6316
+ region
6317
+ });
6417
6318
  return {
6418
- pk: "bundle_patches",
6419
- sk: row.id,
6420
- version,
6421
- gsi1pk: patchOwnerPartition(row.bundle_id),
6422
- gsi1sk: orderKey,
6423
- row
6424
- };
6425
- };
6426
- const toDynamoDBChannelItem = (row, version = 1, referenceCount = 0) => ({
6427
- pk: DYNAMODB_CHANNEL_PARTITION,
6428
- sk: row.id,
6429
- version,
6430
- reference_count: referenceCount,
6431
- row
6432
- });
6433
- const releaseScopePartition = (scopeKey) => `release-scope#${scopeKey}`;
6434
- const toDynamoDBReleaseItem = (row, version = 1) => ({
6435
- pk: releaseScopePartition(row.scope_key),
6436
- sk: row.id,
6437
- version,
6438
- row
6439
- });
6440
- const toDynamoDBReleaseCatalogItem = (row, version = 1) => ({
6441
- pk: "release_catalogs",
6442
- sk: row.scope_key,
6443
- version,
6444
- row
6445
- });
6446
- const itemKey = (model, id) => ({
6447
- pk: model,
6448
- sk: id
6449
- });
6450
- const exactDynamoDBId = (where) => {
6451
- if (where?.length !== 1) return void 0;
6452
- const condition = where[0];
6453
- if (Reflect.get(condition, "field") !== "id" || (Reflect.get(condition, "operator") ?? "eq") !== "eq" || Reflect.get(condition, "mode") === "insensitive") return;
6454
- const value = Reflect.get(condition, "value");
6455
- return typeof value === "string" ? value : void 0;
6456
- };
6457
- const exactDynamoDBPatchOwner = (where) => {
6458
- const owners = exactDynamoDBPatchOwners(where);
6459
- return owners?.length === 1 ? owners[0] : void 0;
6460
- };
6461
- const exactDynamoDBPatchOwners = (where) => {
6462
- if (!where || where.some((condition) => Reflect.get(condition, "connector") === "OR")) return;
6463
- const ownerConditions = where.filter((condition) => Reflect.get(condition, "field") === "bundle_id");
6464
- if (ownerConditions.length !== 1) return void 0;
6465
- const condition = ownerConditions[0];
6466
- if (Reflect.get(condition, "mode") === "insensitive") return;
6467
- const operator = Reflect.get(condition, "operator") ?? "eq";
6468
- const value = Reflect.get(condition, "value");
6469
- if (operator === "eq") return typeof value === "string" ? [value] : void 0;
6470
- if (operator === "in" && Array.isArray(value) && value.every((id) => typeof id === "string")) return value;
6471
- };
6472
- const exactDynamoDBBundleIds = (where) => {
6473
- if (!where || where.some((condition) => Reflect.get(condition, "connector") === "OR")) return;
6474
- for (const condition of where) {
6475
- if (Reflect.get(condition, "field") !== "id" || Reflect.get(condition, "mode") === "insensitive") continue;
6476
- const operator = Reflect.get(condition, "operator") ?? "eq";
6477
- const value = Reflect.get(condition, "value");
6478
- if (operator === "eq" && typeof value === "string") return [value];
6479
- if (operator === "in" && Array.isArray(value) && value.every((id) => typeof id === "string")) return value;
6480
- }
6481
- };
6482
- const dynamoDBBundlePageStart = (where, direction) => {
6483
- if ((where ?? []).some((condition) => Reflect.get(condition, "connector") === "OR")) return;
6484
- let result;
6485
- for (const condition of where ?? []) {
6486
- if (Reflect.get(condition, "field") !== "id") continue;
6487
- const operator = Reflect.get(condition, "operator") ?? "eq";
6488
- const value = Reflect.get(condition, "value");
6489
- if (!(direction === "asc" ? operator === "gt" || operator === "gte" : operator === "lt" || operator === "lte") || typeof value !== "string") continue;
6490
- const inclusive = operator === "gte" || operator === "lte";
6491
- if (result === void 0 || (direction === "asc" ? value > result.id : value < result.id) || value === result.id && !inclusive) result = {
6492
- id: value,
6493
- inclusive
6494
- };
6495
- }
6496
- return result;
6497
- };
6498
- const parseStoredItem = (item) => parseDynamoDBItem(item.pk === "release_catalogs" ? boundedDynamoDBCatalogItem(item) : boundedDynamoDBMetadataItem(item));
6499
- const queryItems = async (store, input) => {
6500
- let exclusiveStartKey;
6501
- const items = [];
6502
- do {
6503
- const sortCondition = input.minimumSortKey ? " AND #sortKey >= :minimumSortKey" : "";
6504
- const page = await store.client.send(new _aws_sdk_lib_dynamodb.QueryCommand({
6505
- TableName: store.tableName,
6506
- ...input.indexName ? { IndexName: input.indexName } : {},
6507
- ...input.consistentRead ? { ConsistentRead: true } : {},
6508
- ExclusiveStartKey: exclusiveStartKey,
6509
- KeyConditionExpression: `#partitionKey = :partition${sortCondition}`,
6510
- ExpressionAttributeNames: {
6511
- "#partitionKey": input.indexName ? "gsi1pk" : "pk",
6512
- ...input.minimumSortKey ? { "#sortKey": input.indexName ? "gsi1sk" : "sk" } : {}
6513
- },
6514
- ExpressionAttributeValues: {
6515
- ":partition": input.partition,
6516
- ...input.minimumSortKey ? { ":minimumSortKey": input.minimumSortKey } : {}
6319
+ invalidate: async () => {
6320
+ try {
6321
+ await invalidateCloudFront(client, cloudfrontDistributionId, [`${apiBasePath.replace(/\/+$/, "")}/*`], { shouldWait: shouldWaitForInvalidation });
6322
+ } catch (error) {
6323
+ console.warn("[hot-updater/aws] CloudFront invalidation failed; continuing without cache invalidation.", {
6324
+ distributionId: cloudfrontDistributionId,
6325
+ error: error instanceof Error ? error.message : "Unknown error"
6326
+ });
6517
6327
  }
6518
- }));
6519
- const pageItems = (page.Items ?? []).map(parseStoredItem);
6520
- items.push(...pageItems);
6521
- exclusiveStartKey = page.LastEvaluatedKey;
6522
- } while (exclusiveStartKey !== void 0);
6523
- return items;
6524
- };
6525
- const queryBundleItemsPage = async (store, input) => {
6526
- if (input.limit === 0) return [];
6527
- let exclusiveStartKey;
6528
- const items = [];
6529
- do {
6530
- const startOperator = input.start?.inclusive ? input.direction === "asc" ? ">=" : "<=" : input.direction === "asc" ? ">" : "<";
6531
- const page = await store.client.send(new _aws_sdk_lib_dynamodb.QueryCommand({
6532
- TableName: store.tableName,
6533
- ExclusiveStartKey: exclusiveStartKey,
6534
- KeyConditionExpression: input.start ? `#pk = :pk AND #sk ${startOperator} :start` : "#pk = :pk",
6535
- ExpressionAttributeNames: {
6536
- "#pk": "pk",
6537
- ...input.start ? { "#sk": "sk" } : {}
6538
- },
6539
- ExpressionAttributeValues: {
6540
- ":pk": "bundles",
6541
- ...input.start ? { ":start": input.start.id } : {}
6542
- },
6543
- Limit: Math.max(input.limit - items.length, 100),
6544
- ScanIndexForward: input.direction === "asc"
6545
- }));
6546
- for (const item of (page.Items ?? []).map(parseStoredItem)) {
6547
- if (item.pk === "bundles" && input.matches(item.row)) items.push(item);
6548
- if (items.length === input.limit) return items;
6549
- }
6550
- exclusiveStartKey = page.LastEvaluatedKey;
6551
- } while (exclusiveStartKey !== void 0);
6552
- return items;
6553
- };
6554
- const loadModelItems = (store, model) => queryItems(store, {
6555
- partition: model,
6556
- consistentRead: true
6557
- });
6558
- const loadModelItem = async (store, model, id) => {
6559
- const { Item } = await store.client.send(new _aws_sdk_lib_dynamodb.GetCommand({
6560
- TableName: store.tableName,
6561
- Key: itemKey(model, id),
6562
- ConsistentRead: true
6563
- }));
6564
- return Item === void 0 ? void 0 : parseStoredItem(Item);
6565
- };
6566
- const loadBundleItem = async (store, id) => {
6567
- const item = await loadModelItem(store, "bundles", id);
6568
- return item?.pk === "bundles" ? item : void 0;
6569
- };
6570
- const loadPatchItem = async (store, id) => {
6571
- const item = await loadModelItem(store, "bundle_patches", id);
6572
- return item?.pk === "bundle_patches" ? item : void 0;
6573
- };
6574
- const loadChannelItem = async (store, id) => {
6575
- const item = await loadModelItem(store, DYNAMODB_CHANNEL_PARTITION, id);
6576
- return item?.pk === "channels" ? item : void 0;
6577
- };
6578
- const loadBundleItems = async (store) => {
6579
- return (await loadModelItems(store, "bundles")).filter((item) => item.pk === "bundles");
6580
- };
6581
- const loadPatchItems = async (store) => {
6582
- return (await loadModelItems(store, "bundle_patches")).filter((item) => item.pk === "bundle_patches");
6583
- };
6584
- const loadChannelItems = async (store) => {
6585
- return (await loadModelItems(store, DYNAMODB_CHANNEL_PARTITION)).filter((item) => item.pk === DYNAMODB_CHANNEL_PARTITION);
6586
- };
6587
- const DYNAMODB_RELEASE_ID_PARTITION = "_hot-updater#release-scope-by-id";
6588
- const releaseLocatorItem = (row) => ({
6589
- pk: DYNAMODB_RELEASE_ID_PARTITION,
6590
- sk: row.id,
6591
- scope_key: row.scope_key
6592
- });
6593
- const loadReleaseItem = async (store, id) => {
6594
- const locator = await store.client.send(new _aws_sdk_lib_dynamodb.GetCommand({
6595
- TableName: store.tableName,
6596
- Key: {
6597
- pk: DYNAMODB_RELEASE_ID_PARTITION,
6598
- sk: id
6599
- },
6600
- ConsistentRead: true
6601
- }));
6602
- if (locator.Item === void 0) return void 0;
6603
- const scopeKey = locator.Item.scope_key;
6604
- if (typeof scopeKey !== "string") throw new DynamoDBStoredItemError();
6605
- const result = await store.client.send(new _aws_sdk_lib_dynamodb.GetCommand({
6606
- TableName: store.tableName,
6607
- Key: {
6608
- pk: releaseScopePartition(scopeKey),
6609
- sk: id
6610
6328
  },
6611
- ConsistentRead: true
6612
- }));
6613
- if (result.Item === void 0) throw new DynamoDBStoredItemError();
6614
- const item = parseStoredItem(result.Item);
6615
- if (!isReleaseRow(item.row) || item.sk !== id) throw new DynamoDBStoredItemError();
6616
- return item;
6617
- };
6618
- const loadReleaseItemsByScope = async (store, scopeKey) => (await queryItems(store, {
6619
- partition: releaseScopePartition(scopeKey),
6620
- consistentRead: true
6621
- })).filter((item) => item.pk === releaseScopePartition(scopeKey) && isReleaseRow(item.row));
6622
- const loadReleaseItems = async (store) => {
6623
- let exclusiveStartKey;
6624
- const items = [];
6625
- do {
6626
- const page = await store.client.send(new _aws_sdk_lib_dynamodb.ScanCommand({
6627
- TableName: store.tableName,
6628
- ExclusiveStartKey: exclusiveStartKey,
6629
- ConsistentRead: true,
6630
- FilterExpression: "begins_with(#pk, :prefix)",
6631
- ExpressionAttributeNames: { "#pk": "pk" },
6632
- ExpressionAttributeValues: { ":prefix": "release-scope#" }
6633
- }));
6634
- for (const value of page.Items ?? []) {
6635
- const item = parseStoredItem(value);
6636
- if (isReleaseRow(item.row)) items.push(item);
6637
- }
6638
- exclusiveStartKey = page.LastEvaluatedKey;
6639
- } while (exclusiveStartKey !== void 0);
6640
- return items;
6641
- };
6642
- const loadReleaseCatalogItem = async (store, scopeKey) => {
6643
- const result = await store.client.send(new _aws_sdk_lib_dynamodb.GetCommand({
6644
- TableName: store.tableName,
6645
- Key: itemKey("release_catalogs", scopeKey),
6646
- ConsistentRead: true
6647
- }));
6648
- if (result.Item === void 0) return void 0;
6649
- const item = parseStoredItem(result.Item);
6650
- return item.pk === "release_catalogs" ? item : void 0;
6651
- };
6652
- const loadReleaseCatalogItems = async (store) => (await queryItems(store, {
6653
- partition: "release_catalogs",
6654
- consistentRead: true
6655
- })).filter((item) => item.pk === "release_catalogs");
6656
- const loadChannelByName = async (store, name) => {
6657
- const { Item } = await store.client.send(new _aws_sdk_lib_dynamodb.GetCommand({
6658
- TableName: store.tableName,
6659
- Key: {
6660
- pk: DYNAMODB_CHANNEL_NAME_PARTITION,
6661
- sk: name
6662
- },
6663
- ConsistentRead: true
6664
- }));
6665
- const id = Item?.channel_id;
6666
- if (Item === void 0) return void 0;
6667
- if (typeof id !== "string") throw new DynamoDBStoredItemError();
6668
- const channel = await loadChannelItem(store, id);
6669
- if (channel === void 0 || channel.row.name !== name) throw new DynamoDBStoredItemError();
6670
- return channel;
6671
- };
6672
- const insertDynamoDBChannel = async (store, input) => {
6673
- const existing = await loadChannelByName(store, input.row.name);
6674
- if (existing !== void 0) return {
6675
- row: existing.row,
6676
- inserted: false
6329
+ destroy: () => client.destroy()
6677
6330
  };
6678
- const existingId = await loadChannelItem(store, input.row.id);
6679
- if (existingId !== void 0) {
6680
- if (existingId.row.name === input.row.name) return {
6681
- row: existingId.row,
6682
- inserted: false
6683
- };
6684
- throw new DynamoDBStoredItemError();
6685
- }
6686
- try {
6687
- await commitDynamoDBTransaction(store, [{ Put: {
6688
- TableName: store.tableName,
6689
- Item: boundedDynamoDBMetadataItem(toDynamoDBChannelItem(input.row)),
6690
- ConditionExpression: "attribute_not_exists(#pk)",
6691
- ExpressionAttributeNames: { "#pk": "pk" }
6692
- } }, { Put: {
6693
- TableName: store.tableName,
6694
- Item: {
6695
- pk: DYNAMODB_CHANNEL_NAME_PARTITION,
6696
- sk: input.row.name,
6697
- channel_id: input.row.id
6698
- },
6699
- ConditionExpression: "attribute_not_exists(#pk)",
6700
- ExpressionAttributeNames: { "#pk": "pk" }
6701
- } }]);
6702
- return {
6703
- row: input.row,
6704
- inserted: true
6705
- };
6706
- } catch (error) {
6707
- const canonical = await loadChannelByName(store, input.row.name);
6708
- if (canonical !== void 0) return {
6709
- row: canonical.row,
6710
- inserted: false
6711
- };
6712
- throw error;
6713
- }
6714
- };
6715
- const deleteDynamoDBChannel = async (store, input) => {
6716
- const current = await loadChannelItem(store, input.id);
6717
- if (current === void 0) return {
6718
- deleted: false,
6719
- reason: "not_found"
6720
- };
6721
- if (current.reference_count !== 0) return {
6722
- deleted: false,
6723
- reason: "not_empty"
6724
- };
6725
- try {
6726
- await commitDynamoDBTransaction(store, [{ Delete: {
6727
- TableName: store.tableName,
6728
- Key: itemKey(DYNAMODB_CHANNEL_PARTITION, current.sk),
6729
- ConditionExpression: "#version = :version AND #referenceCount = :zero",
6730
- ExpressionAttributeNames: {
6731
- "#version": "version",
6732
- "#referenceCount": "reference_count"
6733
- },
6734
- ExpressionAttributeValues: {
6735
- ":version": current.version,
6736
- ":zero": 0
6737
- }
6738
- } }, { Delete: {
6739
- TableName: store.tableName,
6740
- Key: {
6741
- pk: DYNAMODB_CHANNEL_NAME_PARTITION,
6742
- sk: current.row.name
6743
- },
6744
- ConditionExpression: "#channelId = :channelId",
6745
- ExpressionAttributeNames: { "#channelId": "channel_id" },
6746
- ExpressionAttributeValues: { ":channelId": current.sk }
6747
- } }]);
6748
- return { deleted: true };
6749
- } catch (error) {
6750
- const latest = await loadChannelItem(store, input.id);
6751
- if (latest !== void 0 && latest.reference_count > 0) return {
6752
- deleted: false,
6753
- reason: "not_empty"
6754
- };
6755
- if (latest === void 0) return {
6756
- deleted: false,
6757
- reason: "not_found"
6758
- };
6759
- throw error;
6760
- }
6761
- };
6762
- const queryOwnerPatchIds = async (store, indexName, bundleId) => {
6763
- return (await queryItems(store, {
6764
- partition: patchOwnerPartition(bundleId),
6765
- indexName
6766
- })).filter((item) => item.pk === "bundle_patches").map(({ sk }) => sk);
6767
- };
6768
- var DynamoDBTransactionLimitError = class extends Error {
6769
- actionCount;
6770
- name = "DynamoDBTransactionLimitError";
6771
- constructor(actionCount) {
6772
- super(`DynamoDB transaction requires ${actionCount} actions; maximum is 100`);
6773
- this.actionCount = actionCount;
6774
- }
6775
6331
  };
6776
- const metadataUpdate = (store, delta) => {
6777
- const entries = Object.entries(delta).filter(([, value]) => value !== 0);
6778
- if (entries.length === 0) return void 0;
6332
+ //#endregion
6333
+ //#region src/dynamoDBStore.ts
6334
+ /** DynamoDB's limits: 100 items and 4 MB a transaction, 400 KB an item, and 2,048- and 1,024-byte keys. */
6335
+ const DYNAMODB_LIMITS = {
6336
+ items: 100,
6337
+ bytes: 4e6,
6338
+ itemBytes: 4e5,
6339
+ keyBytes: {
6340
+ pk: 2048,
6341
+ sk: 1024
6342
+ }
6343
+ };
6344
+ /** BatchGetItem takes 100 keys a call. */
6345
+ const GET_BATCH = 100;
6346
+ /** A column named like a key attribute is stored with a leading dot, which no column name has. */
6347
+ const attribute = (column) => column === "pk" || column === "sk" ? `.${column}` : column;
6348
+ const renamed = (record, rename) => Object.fromEntries(Object.entries(record).map(([name, value]) => [rename(name), value]));
6349
+ const toItem = (key, row) => ({
6350
+ ...renamed(row, attribute),
6351
+ pk: key.pk,
6352
+ sk: key.sk
6353
+ });
6354
+ const toRow = ({ pk: _pk, sk: _sk, ...item }) => renamed(item, (name) => name.replace(/^\./, ""));
6355
+ const idOf = ({ pk, sk }) => JSON.stringify([pk, sk]);
6356
+ const nameOf = (error) => error.name;
6357
+ const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
6358
+ /**
6359
+ * One transaction item. Its expressions bind every column name and value, so
6360
+ * no column clashes with a reserved word; `pk` and `sk` are none.
6361
+ */
6362
+ const toTransactItem = (tableName, op) => {
6779
6363
  const names = {};
6780
6364
  const values = {};
6781
- const updates = [];
6782
- const conditions = [];
6783
- for (const [model, change] of entries) {
6784
- const name = model === "bundles" ? "bundles" : "patches";
6785
- const valueName = model === "bundles" ? "bundle" : "patch";
6786
- names[`#${name}`] = name;
6787
- values[`:${valueName}Delta`] = change;
6788
- updates.push(`#${name} :${valueName}Delta`);
6789
- if (change < 0) {
6790
- values[`:${valueName}Removal`] = -change;
6791
- conditions.push(`#${name} >= :${valueName}Removal`);
6792
- }
6793
- }
6794
- return { Update: {
6795
- TableName: store.tableName,
6796
- Key: {
6797
- pk: "_hot-updater",
6798
- sk: "limits.metadata"
6799
- },
6800
- UpdateExpression: `ADD ${updates.join(", ")}`,
6801
- ...conditions.length > 0 ? { ConditionExpression: conditions.join(" AND ") } : {},
6802
- ExpressionAttributeNames: names,
6803
- ExpressionAttributeValues: values
6804
- } };
6805
- };
6806
- const updateBundleRelation = (store, bundleId, delta, ownedPatchDelta = 0) => {
6807
- const names = {
6808
- "#pk": "pk",
6809
- "#version": "version"
6365
+ const binder = (map, prefix) => (entry) => {
6366
+ const token = `${prefix}${Object.keys(map).length}`;
6367
+ map[token] = entry;
6368
+ return token;
6810
6369
  };
6811
- const values = { ":one": 1 };
6812
- const additions = [];
6813
- let condition = "attribute_exists(#pk)";
6814
- let update = "SET #version = #version + :one";
6815
- if (delta !== 0) {
6816
- names["#relationCount"] = "relation_count";
6817
- values[":relationDelta"] = delta;
6818
- additions.push("#relationCount :relationDelta");
6819
- if (delta < 0) {
6820
- values[":relationRemoval"] = -delta;
6821
- condition += " AND #relationCount >= :relationRemoval";
6822
- }
6823
- }
6824
- if (ownedPatchDelta !== 0) {
6825
- names["#ownedPatchCount"] = "owned_patch_count";
6826
- values[":ownedPatchDelta"] = ownedPatchDelta;
6827
- additions.push("#ownedPatchCount :ownedPatchDelta");
6828
- if (ownedPatchDelta < 0) {
6829
- values[":ownedPatchRemoval"] = -ownedPatchDelta;
6830
- condition += " AND #ownedPatchCount >= :ownedPatchRemoval";
6831
- }
6832
- }
6833
- if (additions.length > 0) update += ` ADD ${additions.join(", ")}`;
6834
- return { Update: {
6835
- TableName: store.tableName,
6836
- Key: itemKey("bundles", bundleId),
6837
- ConditionExpression: condition,
6838
- ExpressionAttributeNames: names,
6839
- ExpressionAttributeValues: values,
6840
- UpdateExpression: update
6841
- } };
6842
- };
6843
- const commitDynamoDBTransaction = async (store, actions) => {
6844
- if (actions.length > 100) throw new DynamoDBTransactionLimitError(actions.length);
6845
- await store.client.send(new _aws_sdk_lib_dynamodb.TransactWriteCommand({ TransactItems: [...actions] }));
6846
- };
6847
- const DYNAMODB_BATCH_GET_ATTEMPTS = 5;
6848
- const DYNAMODB_BATCH_GET_BASE_DELAY_MS = 25;
6849
- var DynamoDBBatchGetExhaustedError = class extends Error {
6850
- unprocessedKeyCount;
6851
- name = "DynamoDBBatchGetExhaustedError";
6852
- constructor(unprocessedKeyCount) {
6853
- super(`DynamoDB did not process ${unprocessedKeyCount} batch-get keys after ${DYNAMODB_BATCH_GET_ATTEMPTS} attempts`);
6854
- this.unprocessedKeyCount = unprocessedKeyCount;
6855
- }
6856
- };
6857
- const waitBeforeRetry = (attempt) => new Promise((resolve) => setTimeout(resolve, DYNAMODB_BATCH_GET_BASE_DELAY_MS * 2 ** attempt));
6858
- const batchGetDynamoDBItems = async (store, keys) => {
6859
- const items = [];
6860
- for (let offset = 0; offset < keys.length; offset += 100) {
6861
- let pending = keys.slice(offset, offset + 100);
6862
- for (let attempt = 0; attempt < DYNAMODB_BATCH_GET_ATTEMPTS && pending.length > 0; attempt++) {
6863
- const { Responses, UnprocessedKeys } = await store.client.send(new _aws_sdk_lib_dynamodb.BatchGetCommand({ RequestItems: { [store.tableName]: {
6864
- ConsistentRead: true,
6865
- Keys: pending
6866
- } } }));
6867
- items.push(...Responses?.[store.tableName] ?? []);
6868
- pending = UnprocessedKeys?.[store.tableName]?.Keys ?? [];
6869
- if (pending.length > 0 && attempt + 1 < DYNAMODB_BATCH_GET_ATTEMPTS) await waitBeforeRetry(attempt);
6870
- }
6871
- if (pending.length > 0) throw new DynamoDBBatchGetExhaustedError(pending.length);
6872
- }
6873
- return items;
6874
- };
6875
- const parseKeyReadItem = (item) => parseDynamoDBItem(boundedDynamoDBMetadataItem(item));
6876
- const loadBundleItemsById = async (store, ids) => {
6877
- const uniqueIds = [...new Set(ids)];
6878
- if (uniqueIds.length === 0) return [];
6879
- return (await batchGetDynamoDBItems(store, uniqueIds.map((id) => itemKey("bundles", id)))).map(parseKeyReadItem).filter((item) => item.pk === "bundles");
6880
- };
6881
- const loadPatchItemsById = async (store, ids) => {
6882
- const uniqueIds = [...new Set(ids)];
6883
- if (uniqueIds.length === 0) return [];
6884
- return (await batchGetDynamoDBItems(store, uniqueIds.map((id) => itemKey("bundle_patches", id)))).map(parseKeyReadItem).filter((item) => item.pk === "bundle_patches");
6885
- };
6886
- const loadMetadataCount = async (store, model) => {
6887
- if (model !== "bundles" && model !== "bundle_patches") return void 0;
6888
- const field = model === "bundles" ? "bundles" : "patches";
6889
- const { Item } = await store.client.send(new _aws_sdk_lib_dynamodb.GetCommand({
6890
- TableName: store.tableName,
6891
- Key: {
6892
- pk: "_hot-updater",
6893
- sk: "limits.metadata"
6894
- },
6895
- ConsistentRead: true,
6896
- ProjectionExpression: "#count",
6897
- ExpressionAttributeNames: { "#count": field }
6898
- }));
6899
- const count = Item?.[field];
6900
- return typeof count === "number" && Number.isSafeInteger(count) && count >= 0 ? count : void 0;
6901
- };
6902
- var DynamoDBPatchIndexConsistencyError = class extends Error {
6903
- bundleId;
6904
- expectedCount;
6905
- name = "DynamoDBPatchIndexConsistencyError";
6906
- constructor(bundleId, expectedCount) {
6907
- super(`DynamoDB patch index did not converge to ${expectedCount} rows for bundle "${bundleId}"`);
6908
- this.bundleId = bundleId;
6909
- this.expectedCount = expectedCount;
6910
- }
6911
- };
6912
- const waitForIndex = (attempt) => new Promise((resolve) => setTimeout(resolve, 20 * (attempt + 1)));
6913
- const queryCompleteOwnerPatches = async (store, indexName, bundleId) => {
6914
- const owner = await loadBundleItem(store, bundleId);
6915
- if (!owner || owner.owned_patch_count === 0) return [];
6916
- for (let attempt = 0; attempt < 3; attempt++) {
6917
- const patchIds = await queryOwnerPatchIds(store, indexName, bundleId);
6918
- const patchIdSet = new Set(patchIds);
6919
- const patches = (await loadPatchItemsById(store, patchIds)).filter(({ row }) => row.bundle_id === bundleId);
6920
- if (patchIds.length === owner.owned_patch_count && patchIdSet.size === owner.owned_patch_count && patches.length === owner.owned_patch_count && patches.every(({ sk }) => patchIdSet.has(sk))) return patches.map(({ row }) => row).sort((left, right) => left.order_index - right.order_index || left.id.localeCompare(right.id));
6921
- if (attempt < 2) await waitForIndex(attempt);
6922
- }
6923
- throw new DynamoDBPatchIndexConsistencyError(bundleId, owner.owned_patch_count);
6924
- };
6925
- const queryCompleteOwnersPatches = async (store, indexName, bundleIds) => {
6926
- const owners = (await loadBundleItemsById(store, bundleIds)).filter(({ owned_patch_count }) => owned_patch_count > 0);
6927
- const firstOwner = owners[0];
6928
- if (!firstOwner) return [];
6929
- let failedOwner = firstOwner;
6930
- for (let attempt = 0; attempt < 3; attempt++) {
6931
- const candidateIdsByOwner = await Promise.all(owners.map(({ sk }) => queryOwnerPatchIds(store, indexName, sk)));
6932
- const candidateIds = candidateIdsByOwner.flat();
6933
- const patches = await loadPatchItemsById(store, candidateIds);
6934
- const patchesById = new Map(patches.map((patch) => [patch.sk, patch]));
6935
- const mismatchedOwner = owners.find((owner, ownerIndex) => {
6936
- const ids = candidateIdsByOwner[ownerIndex] ?? [];
6937
- const uniqueIds = new Set(ids);
6938
- return ids.length !== owner.owned_patch_count || uniqueIds.size !== owner.owned_patch_count || ids.some((id) => patchesById.get(id)?.row.bundle_id !== owner.sk);
6939
- });
6940
- if (!mismatchedOwner) return patches.map(({ row }) => row);
6941
- failedOwner = mismatchedOwner;
6942
- if (attempt < 2) await waitForIndex(attempt);
6943
- }
6944
- throw new DynamoDBPatchIndexConsistencyError(failedOwner.sk, failedOwner.owned_patch_count);
6945
- };
6946
- const createDynamoDBBundlePatchTable = (store, indexName) => ({ findByBundleIds: (bundleIds) => bundleIds.length === 0 ? Promise.resolve([]) : queryCompleteOwnersPatches(store, indexName, bundleIds) });
6947
- const compare = (left, right) => {
6948
- if (typeof left === "number" && typeof right === "number") return left - right;
6949
- if (left == null) return right == null ? 0 : -1;
6950
- if (right == null) return 1;
6951
- return JSON.stringify(left).localeCompare(JSON.stringify(right));
6952
- };
6953
- const compareString = (actual, expected, mode, predicate) => {
6954
- if (typeof actual !== "string") return false;
6955
- return mode === "insensitive" ? predicate(actual.toLocaleLowerCase(), expected.toLocaleLowerCase()) : predicate(actual, expected);
6956
- };
6957
- const matchesCondition = (row, condition) => {
6958
- const actual = Reflect.get(row, condition.field);
6959
- const expected = Reflect.get(condition, "value");
6960
- switch (Reflect.get(condition, "operator") ?? "eq") {
6961
- case "eq": return typeof expected === "string" ? compareString(actual, expected, Reflect.get(condition, "mode"), (value, query) => value === query) : actual === expected;
6962
- case "ne":
6963
- if (actual == null) return false;
6964
- return typeof expected === "string" ? !compareString(actual, expected, Reflect.get(condition, "mode"), (value, query) => value === query) : actual !== expected;
6965
- case "gt": return actual != null && compare(actual, expected) > 0;
6966
- case "gte": return actual != null && compare(actual, expected) >= 0;
6967
- case "lt": return actual != null && compare(actual, expected) < 0;
6968
- case "lte": return actual != null && compare(actual, expected) <= 0;
6969
- case "in": return Array.isArray(expected) && expected.some((candidate) => candidate === actual);
6970
- case "not_in": return Array.isArray(expected) && (expected.length === 0 || actual != null && expected.every((candidate) => candidate !== actual));
6971
- case "contains": return typeof expected === "string" ? compareString(actual, expected, Reflect.get(condition, "mode"), (value, query) => value.includes(query)) : false;
6972
- case "starts_with": return typeof expected === "string" ? compareString(actual, expected, Reflect.get(condition, "mode"), (value, query) => value.startsWith(query)) : false;
6973
- case "ends_with": return typeof expected === "string" ? compareString(actual, expected, Reflect.get(condition, "mode"), (value, query) => value.endsWith(query)) : false;
6974
- default: return false;
6975
- }
6976
- };
6977
- const matchesDynamoDBWhere = (row, where) => {
6978
- if (!where || where.length === 0) return true;
6979
- let result = matchesCondition(row, where[0]);
6980
- for (const condition of where.slice(1)) {
6981
- const current = matchesCondition(row, condition);
6982
- result = condition.connector === "OR" ? result || current : result && current;
6983
- }
6984
- return result;
6985
- };
6986
- const compareRows = (left, right, orderBy) => {
6987
- for (const clause of orderBy) {
6988
- const leftValue = Reflect.get(left, clause.field);
6989
- const rightValue = Reflect.get(right, clause.field);
6990
- if (leftValue == null || rightValue == null) {
6991
- if (leftValue == null && rightValue == null) continue;
6992
- const nulls = clause.nulls ?? (clause.direction === "asc" ? "last" : "first");
6993
- const order = leftValue == null ? -1 : 1;
6994
- return nulls === "first" ? order : -order;
6995
- }
6996
- const order = compare(leftValue, rightValue);
6997
- if (order !== 0) return clause.direction === "asc" ? order : -order;
6998
- }
6999
- return 0;
7000
- };
7001
- const distinctKey = (row, distinctOn) => JSON.stringify(distinctOn.fields.map((field) => Reflect.get(row, field)));
7002
- const queryDynamoDBRows = (rows, input) => {
7003
- const filtered = rows.filter((row) => matchesDynamoDBWhere(row, input.where));
7004
- const orderBy = input.orderBy;
7005
- const ordered = orderBy ? filtered.toSorted((left, right) => compareRows(left, right, orderBy)) : filtered;
7006
- const distinctOn = input.distinctOn;
7007
- const distinct = distinctOn ? (() => {
7008
- const seen = /* @__PURE__ */ new Set();
7009
- return ordered.filter((row) => {
7010
- const key = distinctKey(row, distinctOn);
7011
- if (seen.has(key)) return false;
7012
- seen.add(key);
7013
- return true;
7014
- });
7015
- })() : ordered;
7016
- const offset = input.offset ?? 0;
7017
- return distinct.slice(offset, offset + (input.limit ?? 100));
7018
- };
7019
- const countDistinctDynamoDBRows = (rows, fields) => {
7020
- if (fields === void 0) return rows.length;
7021
- return new Set(rows.map((row) => JSON.stringify(fields.map((field) => Reflect.get(row, field))))).size;
7022
- };
7023
- const createDynamoDBBundle = async (store, row) => {
7024
- const counter = metadataUpdate(store, { bundles: 1 });
7025
- if (!counter) return;
7026
- await commitDynamoDBTransaction(store, [counter, { Put: {
7027
- TableName: store.tableName,
7028
- Item: boundedDynamoDBMetadataItem(toDynamoDBBundleItem(row)),
7029
- ConditionExpression: "attribute_not_exists(#pk)",
7030
- ExpressionAttributeNames: { "#pk": "pk" }
7031
- } }]);
7032
- };
7033
- const createDynamoDBPatch = async (store, row) => {
7034
- const bundleIds = [.../* @__PURE__ */ new Set([row.bundle_id, row.base_bundle_id])];
7035
- const counter = metadataUpdate(store, { bundle_patches: 1 });
7036
- if (!counter) return;
7037
- await commitDynamoDBTransaction(store, [
7038
- counter,
7039
- ...bundleIds.map((bundleId) => updateBundleRelation(store, bundleId, 1, bundleId === row.bundle_id ? 1 : 0)),
7040
- { Put: {
7041
- TableName: store.tableName,
7042
- Item: boundedDynamoDBMetadataItem(toDynamoDBPatchItem(row)),
7043
- ConditionExpression: "attribute_not_exists(#pk)",
7044
- ExpressionAttributeNames: { "#pk": "pk" }
7045
- } }
7046
- ]);
7047
- };
7048
- const replaceDynamoDBBundle = async (store, current, row) => {
7049
- await store.client.send(new _aws_sdk_lib_dynamodb.PutCommand({
7050
- TableName: store.tableName,
7051
- Item: boundedDynamoDBMetadataItem(toDynamoDBBundleItem(row, current.version + 1, current.relation_count, current.owned_patch_count)),
7052
- ConditionExpression: "#version = :currentVersion",
7053
- ExpressionAttributeNames: { "#version": "version" },
7054
- ExpressionAttributeValues: { ":currentVersion": current.version }
7055
- }));
7056
- };
7057
- const deleteAction = (store, item) => ({ Delete: {
7058
- TableName: store.tableName,
7059
- Key: {
7060
- pk: item.pk,
7061
- sk: item.sk
7062
- },
7063
- ConditionExpression: "#version = :currentVersion",
7064
- ExpressionAttributeNames: { "#version": "version" },
7065
- ExpressionAttributeValues: { ":currentVersion": item.version }
7066
- } });
7067
- const deleteDynamoDBPatch = async (store, item) => {
7068
- const counter = metadataUpdate(store, { bundle_patches: -1 });
7069
- if (!counter) return;
7070
- const bundleIds = [.../* @__PURE__ */ new Set([item.row.bundle_id, item.row.base_bundle_id])];
7071
- await commitDynamoDBTransaction(store, [
7072
- counter,
7073
- ...bundleIds.map((id) => updateBundleRelation(store, id, -1, id === item.row.bundle_id ? -1 : 0)),
7074
- deleteAction(store, item)
7075
- ]);
7076
- };
7077
- const deleteBundle = async (store, bundle, patches) => {
7078
- const counter = metadataUpdate(store, {
7079
- bundles: -1,
7080
- bundle_patches: -patches.length
6370
+ const name = binder(names, "#n");
6371
+ const value = binder(values, ":v");
6372
+ const { condition } = op;
6373
+ const guard = condition === void 0 ? void 0 : "v" in condition ? `${name("_v")} = ${value(condition.v)}` : `${condition.exists ? "attribute_exists" : "attribute_not_exists"}(pk)`;
6374
+ const item = (fields) => ({
6375
+ TableName: tableName,
6376
+ ConditionExpression: guard,
6377
+ ...fields,
6378
+ ExpressionAttributeNames: Object.keys(names).length ? names : void 0,
6379
+ ExpressionAttributeValues: Object.keys(values).length ? values : void 0
7081
6380
  });
7082
- if (!counter) return;
7083
- const otherEndpointCounts = /* @__PURE__ */ new Map();
7084
- for (const { row } of patches) for (const id of /* @__PURE__ */ new Set([row.bundle_id, row.base_bundle_id])) {
7085
- if (id === bundle.sk) continue;
7086
- const current = otherEndpointCounts.get(id) ?? {
7087
- owned: 0,
7088
- relations: 0
7089
- };
7090
- otherEndpointCounts.set(id, {
7091
- owned: current.owned + (id === row.bundle_id ? 1 : 0),
7092
- relations: current.relations + 1
7093
- });
7094
- }
7095
- await commitDynamoDBTransaction(store, [
7096
- counter,
7097
- ...[...otherEndpointCounts].map(([id, count]) => updateBundleRelation(store, id, -count.relations, -count.owned)),
7098
- ...patches.map((patch) => deleteAction(store, patch)),
7099
- deleteAction(store, bundle)
7100
- ]);
7101
- };
7102
- const deleteDynamoDBBundles = async (store, bundles, allPatches) => {
7103
- let patches = [...allPatches];
7104
- const versionIncrements = /* @__PURE__ */ new Map();
7105
- for (const originalBundle of bundles) {
7106
- const bundle = {
7107
- ...originalBundle,
7108
- version: originalBundle.version + (versionIncrements.get(originalBundle.sk) ?? 0)
7109
- };
7110
- const related = patches.filter(({ row }) => row.bundle_id === bundle.sk || row.base_bundle_id === bundle.sk);
7111
- await deleteBundle(store, bundle, related);
7112
- const updatedBundleIds = new Set(related.flatMap(({ row }) => [row.bundle_id, row.base_bundle_id]));
7113
- updatedBundleIds.delete(bundle.sk);
7114
- for (const id of updatedBundleIds) versionIncrements.set(id, (versionIncrements.get(id) ?? 0) + 1);
7115
- const removedPatchIds = new Set(related.map(({ sk }) => sk));
7116
- patches = patches.filter(({ sk }) => !removedPatchIds.has(sk));
7117
- }
7118
- };
7119
- var DynamoDBUnsupportedModelError = class extends Error {
7120
- name = "DynamoDBUnsupportedModelError";
7121
- constructor() {
7122
- super("DynamoDB received an unsupported database model");
7123
- }
7124
- };
7125
- const distinctFields = (fields) => fields;
7126
- const exactDynamoDBField = (where, fieldName) => {
7127
- const conditions = (where ?? []).filter((condition) => Reflect.get(condition, "field") === fieldName);
7128
- if (conditions.length !== 1) return void 0;
7129
- const condition = conditions[0];
7130
- if ((Reflect.get(condition, "operator") ?? "eq") !== "eq" || Reflect.get(condition, "mode") === "insensitive") return;
7131
- const value = Reflect.get(condition, "value");
7132
- return typeof value === "string" ? value : void 0;
7133
- };
7134
- const createDynamoDBCrud = (store, updateIndexName) => ({
7135
- recordInsights: (input) => recordDynamoDBInsightsEvent(store, input),
7136
- findLatestInsightsEvents: (input) => createDynamoDBInsightsTable(store).findLatestEvents(input),
7137
- countLatestInsightsEvents: (input) => createDynamoDBInsightsTable(store).countLatestEvents(input),
7138
- async create(input) {
7139
- switch (input.model) {
7140
- case "bundles":
7141
- await createDynamoDBBundle(store, input.data);
7142
- return input.data;
7143
- case "bundle_patches":
7144
- await createDynamoDBPatch(store, input.data);
7145
- return input.data;
7146
- case "releases": {
7147
- const channel = await loadChannelItem(store, input.data.channel_id);
7148
- if (channel === void 0) throw new DynamoDBStoredItemError();
7149
- await commitDynamoDBTransaction(store, [
7150
- updateChannelReferenceCount(store, channel, 1),
7151
- { Put: {
7152
- TableName: store.tableName,
7153
- Item: boundedDynamoDBMetadataItem(toDynamoDBReleaseItem(input.data)),
7154
- ConditionExpression: "attribute_not_exists(#pk)",
7155
- ExpressionAttributeNames: { "#pk": "pk" }
7156
- } },
7157
- { Put: {
7158
- TableName: store.tableName,
7159
- Item: releaseLocatorItem(input.data),
7160
- ConditionExpression: "attribute_not_exists(#pk)",
7161
- ExpressionAttributeNames: { "#pk": "pk" }
7162
- } }
7163
- ]);
7164
- return input.data;
7165
- }
7166
- case "release_catalogs":
7167
- await store.client.send(new _aws_sdk_lib_dynamodb.PutCommand({
7168
- TableName: store.tableName,
7169
- Item: boundedDynamoDBCatalogItem(toDynamoDBReleaseCatalogItem(input.data)),
7170
- ConditionExpression: "attribute_not_exists(#pk)",
7171
- ExpressionAttributeNames: { "#pk": "pk" }
7172
- }));
7173
- return input.data;
7174
- case "channels": return (await insertDynamoDBChannel(store, {
7175
- row: input.data,
7176
- onConflict: "returnExisting"
7177
- })).row;
7178
- }
7179
- throw new DynamoDBUnsupportedModelError();
7180
- },
7181
- async update(input) {
7182
- if (input.model === "releases") {
7183
- const id = exactDynamoDBId(input.where);
7184
- if (id === void 0) throw new DynamoDBUnsupportedModelError();
7185
- const current = await loadReleaseItem(store, id);
7186
- if (current === void 0) return null;
7187
- const updated = {
7188
- ...current.row,
7189
- ...input.update
7190
- };
7191
- const nextPartition = releaseScopePartition(updated.scope_key);
7192
- const moved = current.pk !== nextPartition;
7193
- await commitDynamoDBTransaction(store, [
7194
- ...moved ? [{ Delete: {
7195
- TableName: store.tableName,
7196
- Key: {
7197
- pk: current.pk,
7198
- sk: current.sk
7199
- },
7200
- ConditionExpression: "#version = :version",
7201
- ExpressionAttributeNames: { "#version": "version" },
7202
- ExpressionAttributeValues: { ":version": current.version }
7203
- } }] : [],
7204
- { Put: {
7205
- TableName: store.tableName,
7206
- Item: boundedDynamoDBMetadataItem(toDynamoDBReleaseItem(updated, current.version + 1)),
7207
- ConditionExpression: moved ? "attribute_not_exists(#pk)" : "#version = :version",
7208
- ExpressionAttributeNames: moved ? { "#pk": "pk" } : { "#version": "version" },
7209
- ...moved ? {} : { ExpressionAttributeValues: { ":version": current.version } }
7210
- } },
7211
- { Put: {
7212
- TableName: store.tableName,
7213
- Item: releaseLocatorItem(updated),
7214
- ConditionExpression: "#scopeKey = :scopeKey",
7215
- ExpressionAttributeNames: { "#scopeKey": "scope_key" },
7216
- ExpressionAttributeValues: { ":scopeKey": current.row.scope_key }
7217
- } }
7218
- ]);
7219
- return updated;
7220
- }
7221
- if (input.model === "release_catalogs") {
7222
- const scopeKey = exactDynamoDBField(input.where, "scope_key");
7223
- if (scopeKey === void 0) throw new DynamoDBUnsupportedModelError();
7224
- const current = await loadReleaseCatalogItem(store, scopeKey);
7225
- if (current === void 0) return null;
7226
- const updated = {
7227
- ...current.row,
7228
- ...input.update
7229
- };
7230
- await store.client.send(new _aws_sdk_lib_dynamodb.PutCommand({
7231
- TableName: store.tableName,
7232
- Item: boundedDynamoDBCatalogItem(toDynamoDBReleaseCatalogItem(updated, current.version + 1)),
7233
- ConditionExpression: "#version = :version",
7234
- ExpressionAttributeNames: { "#version": "version" },
7235
- ExpressionAttributeValues: { ":version": current.version }
7236
- }));
7237
- return updated;
7238
- }
7239
- if (input.model !== "bundles") throw new DynamoDBUnsupportedModelError();
7240
- const id = exactDynamoDBId(input.where);
7241
- const current = id === void 0 ? (await loadBundleItems(store)).find(({ row }) => matchesDynamoDBWhere(row, input.where)) : await loadBundleItem(store, id);
7242
- if (!current) return null;
7243
- const updated = {
7244
- ...current.row,
7245
- ...input.update
7246
- };
7247
- await replaceDynamoDBBundle(store, current, updated);
7248
- return updated;
7249
- },
7250
- async delete(input) {
7251
- if (input.model === "channels") {
7252
- const id = exactDynamoDBId(input.where);
7253
- if (id !== void 0) await deleteDynamoDBChannel(store, { id });
7254
- return;
7255
- }
7256
- if (input.model === "bundle_patches") {
7257
- const items = (await loadPatchItems(store)).filter(({ row }) => matchesDynamoDBWhere(row, input.where));
7258
- for (const item of items) await deleteDynamoDBPatch(store, item);
7259
- return;
7260
- }
7261
- if (input.model === "releases") {
7262
- const items = (await loadReleaseItems(store)).filter(({ row }) => matchesDynamoDBWhere(row, input.where));
7263
- for (const item of items) {
7264
- const channel = await loadChannelItem(store, item.row.channel_id);
7265
- if (channel === void 0) throw new DynamoDBStoredItemError();
7266
- await commitDynamoDBTransaction(store, [
7267
- updateChannelReferenceCount(store, channel, -1),
7268
- deleteAction(store, item),
7269
- { Delete: {
7270
- TableName: store.tableName,
7271
- Key: {
7272
- pk: DYNAMODB_RELEASE_ID_PARTITION,
7273
- sk: item.sk
7274
- },
7275
- ConditionExpression: "#scopeKey = :scopeKey",
7276
- ExpressionAttributeNames: { "#scopeKey": "scope_key" },
7277
- ExpressionAttributeValues: { ":scopeKey": item.row.scope_key }
7278
- } }
7279
- ]);
7280
- }
7281
- return;
7282
- }
7283
- if (input.model !== "bundles") throw new DynamoDBUnsupportedModelError();
7284
- const bundleItems = (await loadBundleItems(store)).filter(({ row }) => matchesDynamoDBWhere(row, input.where));
7285
- if (bundleItems.length === 0) return;
7286
- await deleteDynamoDBBundles(store, bundleItems, await loadPatchItems(store));
7287
- },
7288
- async count(input) {
7289
- if ((input.where === void 0 || input.where.length === 0) && input.distinct === void 0) {
7290
- const count = await loadMetadataCount(store, input.model);
7291
- if (count !== void 0) return count;
7292
- }
7293
- switch (input.model) {
7294
- case "bundles": {
7295
- const rows = (await loadBundleItems(store)).map(({ row }) => row).filter((row) => matchesDynamoDBWhere(row, input.where));
7296
- return countDistinctDynamoDBRows(rows, distinctFields(input.distinct));
7297
- }
7298
- case "bundle_patches": {
7299
- const ownerId = exactDynamoDBPatchOwner(input.where);
7300
- const rows = (ownerId ? await queryCompleteOwnerPatches(store, updateIndexName, ownerId) : (await loadPatchItems(store)).map(({ row }) => row)).filter((row) => matchesDynamoDBWhere(row, input.where));
7301
- return countDistinctDynamoDBRows(rows, distinctFields(input.distinct));
7302
- }
7303
- case "releases": {
7304
- const scopeKey = exactDynamoDBField(input.where, "scope_key");
7305
- const rows = (scopeKey === void 0 ? await loadReleaseItems(store) : await loadReleaseItemsByScope(store, scopeKey)).map(({ row }) => row).filter((row) => matchesDynamoDBWhere(row, input.where));
7306
- return countDistinctDynamoDBRows(rows, distinctFields(input.distinct));
7307
- }
7308
- }
7309
- throw new DynamoDBUnsupportedModelError();
7310
- },
7311
- async findOne(input) {
7312
- const id = exactDynamoDBId(input.where);
7313
- switch (input.model) {
7314
- case "bundles":
7315
- if (id !== void 0) return (await loadBundleItem(store, id))?.row ?? null;
7316
- return (await loadBundleItems(store)).map(({ row }) => row).find((row) => matchesDynamoDBWhere(row, input.where)) ?? null;
7317
- case "bundle_patches":
7318
- if (id !== void 0) return (await loadPatchItem(store, id))?.row ?? null;
7319
- {
7320
- const ownerId = exactDynamoDBPatchOwner(input.where);
7321
- if (ownerId === void 0) break;
7322
- return (await queryCompleteOwnerPatches(store, updateIndexName, ownerId)).find((row) => matchesDynamoDBWhere(row, input.where)) ?? null;
7323
- }
7324
- case "channels":
7325
- if (id !== void 0) return (await loadChannelItem(store, id))?.row ?? null;
7326
- return (await loadChannelItems(store)).map(({ row }) => row).find((row) => matchesDynamoDBWhere(row, input.where)) ?? null;
7327
- case "releases":
7328
- if (id !== void 0) return (await loadReleaseItem(store, id))?.row ?? null;
7329
- {
7330
- const scopeKey = exactDynamoDBField(input.where, "scope_key");
7331
- return (scopeKey === void 0 ? await loadReleaseItems(store) : await loadReleaseItemsByScope(store, scopeKey)).map(({ row }) => row).find((row) => matchesDynamoDBWhere(row, input.where)) ?? null;
7332
- }
7333
- case "release_catalogs": {
7334
- const scopeKey = exactDynamoDBField(input.where, "scope_key");
7335
- if (scopeKey !== void 0) return (await loadReleaseCatalogItem(store, scopeKey))?.row ?? null;
7336
- return (await loadReleaseCatalogItems(store)).map(({ row }) => row).find((row) => matchesDynamoDBWhere(row, input.where)) ?? null;
7337
- }
7338
- }
7339
- if (input.model === "bundle_patches") return (await loadPatchItems(store)).map(({ row }) => row).find((row) => matchesDynamoDBWhere(row, input.where)) ?? null;
7340
- throw new DynamoDBUnsupportedModelError();
7341
- },
7342
- async findMany(input) {
7343
- if (input.limit === 0) return [];
7344
- switch (input.model) {
7345
- case "bundles": {
7346
- const ids = exactDynamoDBBundleIds(input.where);
7347
- if (ids !== void 0) return queryDynamoDBRows((await loadBundleItemsById(store, ids)).map(({ row }) => row), input);
7348
- const orderBy = input.orderBy;
7349
- const direction = orderBy?.[0]?.direction;
7350
- if ((input.offset ?? 0) === 0 && input.distinctOn === void 0 && orderBy?.length === 1 && orderBy[0]?.field === "id" && (direction === "asc" || direction === "desc")) return (await queryBundleItemsPage(store, {
7351
- direction,
7352
- limit: input.limit ?? 100,
7353
- matches: (row) => matchesDynamoDBWhere(row, input.where),
7354
- start: dynamoDBBundlePageStart(input.where, direction)
7355
- })).map(({ row }) => row);
7356
- return queryDynamoDBRows((await loadBundleItems(store)).map(({ row }) => row), input);
7357
- }
7358
- case "bundle_patches": {
7359
- const ownerIds = exactDynamoDBPatchOwners(input.where);
7360
- if (ownerIds !== void 0) {
7361
- const ownerId = ownerIds.length === 1 ? ownerIds[0] : void 0;
7362
- return queryDynamoDBRows(ownerId !== void 0 ? await queryCompleteOwnerPatches(store, updateIndexName, ownerId) : await queryCompleteOwnersPatches(store, updateIndexName, ownerIds), input);
7363
- }
7364
- return queryDynamoDBRows((await loadPatchItems(store)).map(({ row }) => row), input);
7365
- }
7366
- case "channels": return queryDynamoDBRows((await loadChannelItems(store)).map(({ row }) => row), input);
7367
- case "releases": {
7368
- const scopeKey = exactDynamoDBField(input.where, "scope_key");
7369
- return queryDynamoDBRows((scopeKey === void 0 ? await loadReleaseItems(store) : await loadReleaseItemsByScope(store, scopeKey)).map(({ row }) => row), input);
7370
- }
7371
- case "release_catalogs": return queryDynamoDBRows((await loadReleaseCatalogItems(store)).map(({ row }) => row), input);
7372
- }
7373
- throw new DynamoDBUnsupportedModelError();
7374
- },
7375
- insertChannel: (input) => insertDynamoDBChannel(store, input),
7376
- deleteChannel: (input) => deleteDynamoDBChannel(store, input)
7377
- });
7378
- var DynamoDBDuplicatePatchError = class extends Error {
7379
- patchId;
7380
- name = "DynamoDBDuplicatePatchError";
7381
- constructor(patchId) {
7382
- super(`DynamoDB bundle mutation contains duplicate patch "${patchId}"`);
7383
- this.patchId = patchId;
7384
- }
7385
- };
7386
- const putNewBundle = (store, row, relationCount, ownedPatchCount = relationCount) => ({ Put: {
7387
- TableName: store.tableName,
7388
- Item: boundedDynamoDBMetadataItem(toDynamoDBBundleItem(row, 1, relationCount, ownedPatchCount)),
7389
- ConditionExpression: "attribute_not_exists(#pk)",
7390
- ExpressionAttributeNames: { "#pk": "pk" }
7391
- } });
7392
- const putUpdatedBundle = (store, current, row, relationCount, ownedPatchCount) => ({ Put: {
7393
- TableName: store.tableName,
7394
- Item: boundedDynamoDBMetadataItem(toDynamoDBBundleItem(row, current.version + 1, relationCount, ownedPatchCount)),
7395
- ConditionExpression: "#version = :currentVersion",
7396
- ExpressionAttributeNames: { "#version": "version" },
7397
- ExpressionAttributeValues: { ":currentVersion": current.version }
7398
- } });
7399
- const putPatch = (store, row, current) => ({ Put: {
7400
- TableName: store.tableName,
7401
- Item: boundedDynamoDBMetadataItem(toDynamoDBPatchItem(row, (current?.version ?? 0) + 1)),
7402
- ConditionExpression: current ? "#version = :currentVersion" : "attribute_not_exists(#pk)",
7403
- ExpressionAttributeNames: current ? { "#version": "version" } : { "#pk": "pk" },
7404
- ...current ? { ExpressionAttributeValues: { ":currentVersion": current.version } } : {}
7405
- } });
7406
- const deletePatch = (store, item) => ({ Delete: {
7407
- TableName: store.tableName,
7408
- Key: itemKey(item.pk, item.sk),
7409
- ConditionExpression: "#version = :currentVersion",
7410
- ExpressionAttributeNames: { "#version": "version" },
7411
- ExpressionAttributeValues: { ":currentVersion": item.version }
7412
- } });
7413
- var DynamoDBCommitStateError = class extends Error {
7414
- name = "DynamoDBCommitStateError";
7415
- constructor(message) {
7416
- super(message);
7417
- }
7418
- };
7419
- const rowsEqual = (left, right) => JSON.stringify(left) === JSON.stringify(right);
7420
- const putChannel = (store, row, current, referenceCount) => ({ Put: {
7421
- TableName: store.tableName,
7422
- Item: boundedDynamoDBMetadataItem(toDynamoDBChannelItem(row, (current?.version ?? 0) + 1, referenceCount)),
7423
- ConditionExpression: current ? "#version = :version" : "attribute_not_exists(#pk)",
7424
- ExpressionAttributeNames: current ? { "#version": "version" } : { "#pk": "pk" },
7425
- ...current ? { ExpressionAttributeValues: { ":version": current.version } } : {}
7426
- } });
7427
- const deleteChannelActions = (store, current) => [{ Delete: {
7428
- TableName: store.tableName,
7429
- Key: itemKey(DYNAMODB_CHANNEL_PARTITION, current.sk),
7430
- ConditionExpression: "#version = :version",
7431
- ExpressionAttributeNames: { "#version": "version" },
7432
- ExpressionAttributeValues: { ":version": current.version }
7433
- } }, { Delete: {
7434
- TableName: store.tableName,
7435
- Key: {
7436
- pk: DYNAMODB_CHANNEL_NAME_PARTITION,
7437
- sk: current.row.name
7438
- },
7439
- ConditionExpression: "#channelId = :channelId",
7440
- ExpressionAttributeNames: { "#channelId": "channel_id" },
7441
- ExpressionAttributeValues: { ":channelId": current.sk }
7442
- } }];
7443
- const updateChannelReferenceCount = (store, current, delta) => ({ Update: {
7444
- TableName: store.tableName,
7445
- Key: itemKey(DYNAMODB_CHANNEL_PARTITION, current.sk),
7446
- ConditionExpression: "#row.#name = :name" + (delta < 0 ? " AND #referenceCount >= :removal" : ""),
7447
- UpdateExpression: "SET #version = #version + :one ADD #referenceCount :delta",
7448
- ExpressionAttributeNames: {
7449
- "#row": "row",
7450
- "#name": "name",
7451
- "#version": "version",
7452
- "#referenceCount": "reference_count"
7453
- },
7454
- ExpressionAttributeValues: {
7455
- ":name": current.row.name,
7456
- ":one": 1,
7457
- ":delta": delta,
7458
- ...delta < 0 ? { ":removal": -delta } : {}
7459
- }
7460
- } });
7461
- const loadApiKeyByHash = async (store, hash) => {
7462
- const { Item } = await store.client.send(new _aws_sdk_lib_dynamodb.GetCommand({
7463
- TableName: store.tableName,
7464
- Key: {
7465
- pk: DYNAMODB_API_KEY_HASH_PARTITION,
7466
- sk: hash
7467
- },
7468
- ConsistentRead: true
7469
- }));
7470
- if (Item === void 0) return null;
7471
- const id = Item.api_key_id;
7472
- if (typeof id !== "string") throw new DynamoDBStoredItemError();
7473
- return loadApiKey(store, id);
7474
- };
7475
- const compileAndCommitDynamoDBChanges = async (store, input) => {
7476
- const [originalBundleItems, originalPatchItems, originalChannelItems, originalReleaseItems, originalReleaseCatalogItems] = await Promise.all([
7477
- loadBundleItems(store),
7478
- loadPatchItems(store),
7479
- loadChannelItems(store),
7480
- loadReleaseItems(store),
7481
- loadReleaseCatalogItems(store)
7482
- ]);
7483
- const bundles = new Map(originalBundleItems.map(({ sk, row }) => [sk, row]));
7484
- const patches = new Map(originalPatchItems.map(({ sk, row }) => [sk, row]));
7485
- const channels = new Map(originalChannelItems.map(({ sk, row }) => [sk, row]));
7486
- const channelNames = new Map(originalChannelItems.map(({ sk, row }) => [row.name, sk]));
7487
- const releases = new Map(originalReleaseItems.map(({ sk, row }) => [sk, row]));
7488
- const releaseCatalogs = new Map(originalReleaseCatalogItems.map(({ sk, row }) => [sk, row]));
7489
- const originalApiKeys = /* @__PURE__ */ new Map();
7490
- const apiKeys = /* @__PURE__ */ new Map();
7491
- const apiKeyHashes = /* @__PURE__ */ new Map();
7492
- const rememberApiKey = (value) => {
7493
- if (value === null || apiKeys.has(value.row.id)) return;
7494
- originalApiKeys.set(value.row.id, value);
7495
- apiKeys.set(value.row.id, value);
7496
- apiKeyHashes.set(value.row.hash, value.row.id);
6381
+ const Key = {
6382
+ pk: op.key.pk,
6383
+ sk: op.key.sk
7497
6384
  };
7498
- const requireReleaseReferences = (row) => {
7499
- if (!channels.has(row.channel_id)) throw new DynamoDBCommitStateError(`Release "${row.id}" references an invalid channel`);
7500
- if (row.bundle_id !== null) {
7501
- const bundle = bundles.get(row.bundle_id);
7502
- if (bundle === void 0 || bundle.platform !== row.platform) throw new DynamoDBCommitStateError(`Release "${row.id}" references an invalid bundle`);
7503
- }
7504
- };
7505
- for (const expectation of input.expectations ?? []) {
7506
- const actualVersion = expectation.model === "releases" ? releases.get(expectation.id)?.revision ?? null : releaseCatalogs.get(expectation.scopeKey)?.generation ?? null;
7507
- const expectedVersion = expectation.model === "releases" ? expectation.revision : expectation.generation;
7508
- if (actualVersion !== expectedVersion) return {
7509
- committed: false,
7510
- conflict: {
7511
- actualVersion,
7512
- changeIndex: -1,
7513
- expectedVersion,
7514
- key: expectation.model === "releases" ? expectation.id : expectation.scopeKey,
7515
- model: expectation.model,
7516
- reason: "version_conflict"
7517
- }
7518
- };
7519
- }
7520
- for (const [changeIndex, change] of input.changes.entries()) switch (change.model) {
7521
- case "channels":
7522
- if (change.operation === "insert") {
7523
- if (channelNames.get(change.row.name) !== void 0) break;
7524
- if (channels.get(change.row.id) !== void 0) throw new DynamoDBCommitStateError(`Channel id "${change.row.id}" already exists`);
7525
- channels.set(change.row.id, change.row);
7526
- channelNames.set(change.row.name, change.row.id);
7527
- } else {
7528
- const current = channels.get(change.where.id);
7529
- if (current === void 0) break;
7530
- if ([...releases.values()].some(({ channel_id }) => channel_id === change.where.id)) return {
7531
- committed: false,
7532
- conflict: {
7533
- changeIndex,
7534
- reason: "referenced"
7535
- }
7536
- };
7537
- channels.delete(change.where.id);
7538
- channelNames.delete(current.name);
7539
- }
7540
- break;
7541
- case "bundles":
7542
- if (change.operation === "insert") {
7543
- if (bundles.has(change.row.id)) throw new DynamoDBCommitStateError(`Bundle "${change.row.id}" already exists`);
7544
- bundles.set(change.row.id, change.row);
7545
- } else {
7546
- const current = bundles.get(change.where.id);
7547
- if (current === void 0) {
7548
- if (change.operation === "update") return {
7549
- committed: false,
7550
- conflict: {
7551
- changeIndex,
7552
- reason: "not_found"
7553
- }
7554
- };
7555
- break;
7556
- }
7557
- if (change.operation === "update") {
7558
- const row = {
7559
- ...current,
7560
- ...change.update
7561
- };
7562
- bundles.set(row.id, row);
7563
- } else {
7564
- if ([...releases.values()].some(({ bundle_id }) => bundle_id === change.where.id)) return {
7565
- committed: false,
7566
- conflict: {
7567
- changeIndex,
7568
- reason: "referenced"
7569
- }
7570
- };
7571
- bundles.delete(change.where.id);
7572
- for (const [id, patch] of patches) if (patch.bundle_id === change.where.id || patch.base_bundle_id === change.where.id) patches.delete(id);
7573
- }
7574
- }
7575
- break;
7576
- case "releases":
7577
- if (change.operation === "insert") {
7578
- requireReleaseReferences(change.row);
7579
- if (releases.has(change.row.id)) throw new DynamoDBCommitStateError(`Release "${change.row.id}" already exists`);
7580
- releases.set(change.row.id, change.row);
7581
- } else {
7582
- const current = releases.get(change.where.id);
7583
- if (current === void 0) {
7584
- if (change.operation === "update") return {
7585
- committed: false,
7586
- conflict: {
7587
- changeIndex,
7588
- reason: "not_found"
7589
- }
7590
- };
7591
- break;
7592
- }
7593
- if (change.operation === "update") {
7594
- const row = {
7595
- ...current,
7596
- ...change.update
7597
- };
7598
- requireReleaseReferences(row);
7599
- releases.set(row.id, row);
7600
- } else releases.delete(change.where.id);
7601
- }
7602
- break;
7603
- case "releaseCatalogs":
7604
- if (!channels.has(change.row.channel_id)) throw new DynamoDBCommitStateError(`Release catalog "${change.row.scope_key}" references an invalid channel`);
7605
- releaseCatalogs.set(change.row.scope_key, change.row);
7606
- break;
7607
- case "bundlePatches":
7608
- if (change.operation === "insert") {
7609
- if (!bundles.has(change.row.bundle_id) || !bundles.has(change.row.base_bundle_id)) throw new DynamoDBCommitStateError(`Patch "${change.row.id}" references a missing bundle`);
7610
- if (patches.has(change.row.id)) throw new DynamoDBDuplicatePatchError(change.row.id);
7611
- patches.set(change.row.id, change.row);
7612
- } else for (const [id, patch] of patches) if (patch.bundle_id === change.where.bundleId) patches.delete(id);
7613
- break;
7614
- case "apiKeys": if (change.operation === "insert") {
7615
- rememberApiKey(await loadApiKeyByHash(store, change.row.hash));
7616
- if (apiKeyHashes.has(change.row.hash)) break;
7617
- rememberApiKey(await loadApiKey(store, change.row.id));
7618
- if (apiKeys.has(change.row.id)) throw new DynamoDBCommitStateError(`API key id "${change.row.id}" already exists`);
7619
- const inserted = {
7620
- row: change.row,
7621
- version: 1
7622
- };
7623
- apiKeys.set(change.row.id, inserted);
7624
- apiKeyHashes.set(change.row.hash, change.row.id);
7625
- } else {
7626
- rememberApiKey(await loadApiKey(store, change.where.id));
7627
- const current = apiKeys.get(change.where.id);
7628
- if (current === void 0) return {
7629
- committed: false,
7630
- conflict: {
7631
- changeIndex,
7632
- reason: "not_found"
7633
- }
7634
- };
7635
- apiKeys.set(change.where.id, {
7636
- row: {
7637
- ...current.row,
7638
- revoked_at_ms: change.update.revokedAtMs
7639
- },
7640
- version: current.version
7641
- });
7642
- }
7643
- }
7644
- const relationCounts = /* @__PURE__ */ new Map();
7645
- const ownedPatchCounts = /* @__PURE__ */ new Map();
7646
- for (const patch of patches.values()) {
7647
- for (const id of /* @__PURE__ */ new Set([patch.bundle_id, patch.base_bundle_id])) relationCounts.set(id, (relationCounts.get(id) ?? 0) + 1);
7648
- ownedPatchCounts.set(patch.bundle_id, (ownedPatchCounts.get(patch.bundle_id) ?? 0) + 1);
7649
- }
7650
- const channelReferenceCounts = /* @__PURE__ */ new Map();
7651
- for (const release of releases.values()) channelReferenceCounts.set(release.channel_id, (channelReferenceCounts.get(release.channel_id) ?? 0) + 1);
7652
- const actions = [];
7653
- const originalChannels = new Map(originalChannelItems.map((item) => [item.sk, item]));
7654
- for (const original of originalChannelItems) if (!channels.has(original.sk)) actions.push(...deleteChannelActions(store, original));
7655
- for (const [id, row] of channels) {
7656
- const original = originalChannels.get(id);
7657
- const referenceCount = channelReferenceCounts.get(id) ?? 0;
7658
- if (original === void 0) {
7659
- actions.push(putChannel(store, row, original, referenceCount));
7660
- actions.push({ Put: {
7661
- TableName: store.tableName,
7662
- Item: {
7663
- pk: DYNAMODB_CHANNEL_NAME_PARTITION,
7664
- sk: row.name,
7665
- channel_id: id
7666
- },
7667
- ConditionExpression: "attribute_not_exists(#pk)",
7668
- ExpressionAttributeNames: { "#pk": "pk" }
7669
- } });
7670
- } else {
7671
- if (!rowsEqual(original.row, row)) throw new DynamoDBCommitStateError(`Channel "${id}" cannot be updated`);
7672
- const delta = referenceCount - original.reference_count;
7673
- if (delta !== 0) actions.push(updateChannelReferenceCount(store, original, delta));
7674
- }
7675
- }
7676
- const originalBundles = new Map(originalBundleItems.map((item) => [item.sk, item]));
7677
- for (const original of originalBundleItems) if (!bundles.has(original.sk)) actions.push(deleteAction(store, original));
7678
- for (const [id, row] of bundles) {
7679
- const original = originalBundles.get(id);
7680
- const relationCount = relationCounts.get(id) ?? 0;
7681
- const ownedPatchCount = ownedPatchCounts.get(id) ?? 0;
7682
- if (original === void 0) actions.push(putNewBundle(store, row, relationCount, ownedPatchCount));
7683
- else if (!rowsEqual(original.row, row)) actions.push(putUpdatedBundle(store, original, row, relationCount, ownedPatchCount));
7684
- else {
7685
- const relationDelta = relationCount - original.relation_count;
7686
- const ownedPatchDelta = ownedPatchCount - original.owned_patch_count;
7687
- if (relationDelta !== 0 || ownedPatchDelta !== 0) actions.push(updateBundleRelation(store, id, relationDelta, ownedPatchDelta));
7688
- }
7689
- }
7690
- const originalPatches = new Map(originalPatchItems.map((item) => [item.sk, item]));
7691
- for (const original of originalPatchItems) if (!patches.has(original.sk)) actions.push(deletePatch(store, original));
7692
- for (const [id, row] of patches) {
7693
- const original = originalPatches.get(id);
7694
- if (original === void 0 || !rowsEqual(original.row, row)) actions.push(putPatch(store, row, original));
7695
- }
7696
- const originalReleases = new Map(originalReleaseItems.map((item) => [item.sk, item]));
7697
- for (const original of originalReleaseItems) {
7698
- if (releases.get(original.sk) !== void 0) continue;
7699
- actions.push(deleteAction(store, original));
7700
- actions.push({ Delete: {
7701
- TableName: store.tableName,
7702
- Key: {
7703
- pk: DYNAMODB_RELEASE_ID_PARTITION,
7704
- sk: original.sk
7705
- },
7706
- ConditionExpression: "#scopeKey = :scopeKey",
7707
- ExpressionAttributeNames: { "#scopeKey": "scope_key" },
7708
- ExpressionAttributeValues: { ":scopeKey": original.row.scope_key }
7709
- } });
7710
- }
7711
- for (const [id, row] of releases) {
7712
- const original = originalReleases.get(id);
7713
- if (original === void 0) {
7714
- actions.push({ Put: {
7715
- TableName: store.tableName,
7716
- Item: boundedDynamoDBMetadataItem(toDynamoDBReleaseItem(row)),
7717
- ConditionExpression: "attribute_not_exists(#pk)",
7718
- ExpressionAttributeNames: { "#pk": "pk" }
7719
- } });
7720
- actions.push({ Put: {
7721
- TableName: store.tableName,
7722
- Item: releaseLocatorItem(row),
7723
- ConditionExpression: "attribute_not_exists(#pk)",
7724
- ExpressionAttributeNames: { "#pk": "pk" }
7725
- } });
7726
- } else if (!rowsEqual(original.row, row)) {
7727
- const moved = original.row.scope_key !== row.scope_key;
7728
- if (moved) actions.push(deleteAction(store, original));
7729
- actions.push({ Put: {
7730
- TableName: store.tableName,
7731
- Item: boundedDynamoDBMetadataItem(toDynamoDBReleaseItem(row, original.version + 1)),
7732
- ConditionExpression: moved ? "attribute_not_exists(#pk)" : "#version = :version",
7733
- ExpressionAttributeNames: moved ? { "#pk": "pk" } : { "#version": "version" },
7734
- ...moved ? {} : { ExpressionAttributeValues: { ":version": original.version } }
7735
- } });
7736
- if (moved) actions.push({ Put: {
7737
- TableName: store.tableName,
7738
- Item: releaseLocatorItem(row),
7739
- ConditionExpression: "#scopeKey = :scopeKey",
7740
- ExpressionAttributeNames: { "#scopeKey": "scope_key" },
7741
- ExpressionAttributeValues: { ":scopeKey": original.row.scope_key }
7742
- } });
7743
- }
7744
- }
7745
- const originalReleaseCatalogs = new Map(originalReleaseCatalogItems.map((item) => [item.sk, item]));
7746
- for (const [scopeKey, row] of releaseCatalogs) {
7747
- const original = originalReleaseCatalogs.get(scopeKey);
7748
- if (original === void 0 || !rowsEqual(original.row, row)) actions.push({ Put: {
7749
- TableName: store.tableName,
7750
- Item: boundedDynamoDBCatalogItem(toDynamoDBReleaseCatalogItem(row, (original?.version ?? 0) + 1)),
7751
- ConditionExpression: original ? "#version = :version" : "attribute_not_exists(#pk)",
7752
- ExpressionAttributeNames: original ? { "#version": "version" } : { "#pk": "pk" },
7753
- ...original ? { ExpressionAttributeValues: { ":version": original.version } } : {}
7754
- } });
7755
- }
7756
- const changedReleaseIds = new Set([.../* @__PURE__ */ new Set([...originalReleases.keys(), ...releases.keys()])].filter((id) => {
7757
- const original = originalReleases.get(id)?.row;
7758
- const next = releases.get(id);
7759
- return original === void 0 || next === void 0 || !rowsEqual(original, next);
7760
- }));
7761
- const changedCatalogScopes = new Set([...releaseCatalogs].flatMap(([scopeKey, row]) => {
7762
- const original = originalReleaseCatalogs.get(scopeKey)?.row;
7763
- return original === void 0 || !rowsEqual(original, row) ? [scopeKey] : [];
7764
- }));
7765
- for (const expectation of input.expectations ?? []) {
7766
- if (expectation.model === "releases" && !changedReleaseIds.has(expectation.id)) {
7767
- const original = originalReleases.get(expectation.id);
7768
- actions.push({ ConditionCheck: {
7769
- TableName: store.tableName,
7770
- Key: original === void 0 ? {
7771
- pk: DYNAMODB_RELEASE_ID_PARTITION,
7772
- sk: expectation.id
7773
- } : {
7774
- pk: original.pk,
7775
- sk: original.sk
7776
- },
7777
- ConditionExpression: original === void 0 ? "attribute_not_exists(#pk)" : "#version = :version",
7778
- ExpressionAttributeNames: original === void 0 ? { "#pk": "pk" } : { "#version": "version" },
7779
- ...original === void 0 ? {} : { ExpressionAttributeValues: { ":version": original.version } }
7780
- } });
7781
- }
7782
- if (expectation.model === "releaseCatalogs" && !changedCatalogScopes.has(expectation.scopeKey)) {
7783
- const original = originalReleaseCatalogs.get(expectation.scopeKey);
7784
- actions.push({ ConditionCheck: {
7785
- TableName: store.tableName,
7786
- Key: itemKey("release_catalogs", expectation.scopeKey),
7787
- ConditionExpression: original === void 0 ? "attribute_not_exists(#pk)" : "#version = :version",
7788
- ExpressionAttributeNames: original === void 0 ? { "#pk": "pk" } : { "#version": "version" },
7789
- ...original === void 0 ? {} : { ExpressionAttributeValues: { ":version": original.version } }
7790
- } });
6385
+ switch (op.type) {
6386
+ case "put": return { Put: item({ Item: toItem(op.key, op.value) }) };
6387
+ case "delete": return { Delete: item({ Key }) };
6388
+ case "check": return { ConditionCheck: item({
6389
+ Key,
6390
+ ConditionExpression: guard
6391
+ }) };
6392
+ case "add": {
6393
+ const init = op.init ?? {};
6394
+ return { Update: item({
6395
+ Key,
6396
+ UpdateExpression: `SET ${Object.keys({
6397
+ ...init,
6398
+ ...op.by
6399
+ }).map((column) => {
6400
+ const path = name(attribute(column));
6401
+ const by = op.by[column];
6402
+ if (by === void 0) return `${path} = if_not_exists(${path}, ${value(init[column])})`;
6403
+ return `${path} = if_not_exists(${path}, ${value(Number(init[column] ?? 0))}) + ${value(by)}`;
6404
+ }).join(", ")}`
6405
+ }) };
7791
6406
  }
7792
6407
  }
7793
- const counter = metadataUpdate(store, {
7794
- bundles: bundles.size - originalBundles.size,
7795
- bundle_patches: patches.size - originalPatches.size
7796
- });
7797
- if (counter !== void 0) actions.push(counter);
7798
- for (const [id, current] of apiKeys) {
7799
- const original = originalApiKeys.get(id);
7800
- if (original !== void 0 && rowsEqual(original.row, current.row)) continue;
7801
- actions.push({ Put: {
7802
- TableName: store.tableName,
7803
- Item: boundedDynamoDBMetadataItem(apiKeyItem(current.row, original === void 0 ? 1 : original.version + 1)),
7804
- ConditionExpression: original ? "#version = :version" : "attribute_not_exists(#pk)",
7805
- ExpressionAttributeNames: original ? { "#version": "version" } : { "#pk": "pk" },
7806
- ...original ? { ExpressionAttributeValues: { ":version": original.version } } : {}
7807
- } });
7808
- if (original === void 0) actions.push({ Put: {
7809
- TableName: store.tableName,
7810
- Item: {
7811
- pk: DYNAMODB_API_KEY_HASH_PARTITION,
7812
- sk: current.row.hash,
7813
- api_key_id: current.row.id
7814
- },
7815
- ConditionExpression: "attribute_not_exists(#pk)",
7816
- ExpressionAttributeNames: { "#pk": "pk" }
7817
- } });
7818
- }
7819
- if (actions.length > 0) await commitDynamoDBTransaction(store, actions);
7820
- return { committed: true };
7821
- };
7822
- const isDynamoDBTransactionConflict = (error) => typeof error === "object" && error !== null && Reflect.get(error, "name") === "TransactionCanceledException";
7823
- const createDynamoDBCommit = (store) => async (input) => {
7824
- try {
7825
- return await compileAndCommitDynamoDBChanges(store, input);
7826
- } catch (error) {
7827
- if (!isDynamoDBTransactionConflict(error)) throw error;
7828
- return compileAndCommitDynamoDBChanges(store, input);
7829
- }
7830
- };
7831
- const DYNAMODB_INSIGHTS_PARTITION = "bundle_events";
7832
- const DYNAMODB_INSIGHTS_INSTALLATIONS_PARTITION = "_hot-updater#insights-installations";
7833
- const DYNAMODB_INSIGHTS_EVENT_IDS_PARTITION = "_hot-updater#insights-event-ids";
7834
- const DYNAMODB_INSIGHTS_BUNDLE_PREFIX = "_hot-updater#insights-bundle#";
7835
- const DYNAMODB_API_KEY_PARTITION = "api_keys";
7836
- const DYNAMODB_API_KEY_HASH_PARTITION = "_hot-updater#api-key-hashes";
7837
- const DYNAMODB_INSIGHTS_MOVEMENT_PREFIX = "_hot-updater#insights-movement#";
7838
- const DYNAMODB_INSIGHTS_USER_PREFIX = "_hot-updater#insights-user#";
7839
- const DYNAMODB_INSIGHTS_SCOPE_PREFIX = "_hot-updater#insights-scope#";
7840
- const DYNAMODB_INSIGHTS_RECORD_ATTEMPTS = 3;
7841
- const hasValidBundleEventShape = (value) => {
7842
- const type = field(value, "type");
7843
- const fromBundleId = field(value, "from_bundle_id");
7844
- const metadata = field(value, "metadata");
7845
- if (!(0, _hot_updater_plugin_core.isDatabaseBundleEventMetadata)(metadata)) return false;
7846
- const updateStrategy = metadata.update_strategy;
7847
- return (type === "UPDATE_DOWNLOADED" || type === "UPDATE_APPLIED" || type === "RECOVERED") && typeof fromBundleId === "string" && (updateStrategy === "fingerprint" || updateStrategy === "appVersion") || type === "UNCHANGED" && fromBundleId === null && updateStrategy === null;
7848
- };
7849
- const isBundleEventRow = (value) => typeof value === "object" && value !== null && typeof field(value, "id") === "string" && typeof field(value, "install_id") === "string" && isNullableString(field(value, "from_release_id")) && isNullableString(field(value, "to_release_id")) && typeof field(value, "to_bundle_id") === "string" && (field(value, "platform") === "ios" || field(value, "platform") === "android") && typeof field(value, "received_at_ms") === "number" && hasValidBundleEventShape(value);
7850
- const isApiKeyRow = (value) => typeof value === "object" && value !== null && typeof field(value, "id") === "string" && typeof field(value, "hash") === "string" && typeof field(value, "name") === "string" && typeof field(value, "prefix") === "string" && field(value, "role") === "client" && typeof field(value, "created_at_ms") === "number" && (field(value, "revoked_at_ms") === null || typeof field(value, "revoked_at_ms") === "number");
7851
- const parseOfficialRowItem = (value, partition, isRow) => {
7852
- if (value.pk !== partition || typeof value.sk !== "string" || typeof value.version !== "number" || !isRow(value.row)) throw new DynamoDBStoredItemError();
7853
- return {
7854
- row: value.row,
7855
- version: value.version
7856
- };
7857
6408
  };
7858
- const timestampSortKey = (timestampMs) => Math.trunc(timestampMs).toString().padStart(16, "0");
7859
- const insightsSortKey = (row) => `${timestampSortKey(row.received_at_ms)}#${row.id}`;
7860
- const insightsMovementPartition = (installId) => `${DYNAMODB_INSIGHTS_MOVEMENT_PREFIX}${installId}`;
7861
- const insightsUserPartition = (userId) => `${DYNAMODB_INSIGHTS_USER_PREFIX}${userId}`;
7862
- const insightsScopePartition = (scope) => `${DYNAMODB_INSIGHTS_SCOPE_PREFIX}${(0, node_crypto.createHash)("sha256").update(JSON.stringify([scope.platform, scope.channel]), "utf8").digest("hex")}`;
7863
- const toInsightsScopeItem = (row) => ({
7864
- pk: insightsScopePartition(row),
7865
- sk: row.install_id,
7866
- received_at_ms: row.received_at_ms,
7867
- type: row.type,
7868
- from_bundle_id: row.from_bundle_id,
7869
- to_bundle_id: row.to_bundle_id
7870
- });
7871
- const insightsBundlePartition = (filter) => {
7872
- const scope = JSON.stringify([
7873
- filter.platform,
7874
- filter.channel,
7875
- filter.type,
7876
- filter.type === "RECOVERED" ? filter.fromBundleId : filter.toBundleId
7877
- ]);
7878
- return `${DYNAMODB_INSIGHTS_BUNDLE_PREFIX}${(0, node_crypto.createHash)("sha256").update(scope, "utf8").digest("hex")}`;
7879
- };
7880
- const toInsightsBundleItem = (row) => boundedDynamoDBMetadataItem({
7881
- pk: insightsBundlePartition({
7882
- platform: row.platform,
7883
- channel: row.channel,
7884
- ...row.type === "RECOVERED" ? {
7885
- type: row.type,
7886
- fromBundleId: row.from_bundle_id
7887
- } : {
7888
- type: row.type,
7889
- toBundleId: row.to_bundle_id
7890
- }
7891
- }),
7892
- sk: insightsSortKey(row),
7893
- version: 1,
7894
- row
7895
- });
7896
- const toInsightsEventItem = (row) => {
7897
- const orderKey = insightsSortKey(row);
7898
- return boundedDynamoDBMetadataItem({
7899
- pk: DYNAMODB_INSIGHTS_PARTITION,
7900
- sk: orderKey,
7901
- version: 1,
7902
- row,
7903
- ...(0, _hot_updater_plugin_core.isInsightsMovementEvent)(row) ? {
7904
- gsi1pk: insightsMovementPartition(row.install_id),
7905
- gsi1sk: orderKey
7906
- } : {}
7907
- });
6409
+ /** Errors, and cancellation reasons, that a retry of the whole write can clear. */
6410
+ const TRANSIENT = /* @__PURE__ */ new Set([
6411
+ "TransactionConflict",
6412
+ "ThrottlingError",
6413
+ "ProvisionedThroughputExceeded",
6414
+ "RequestLimitExceeded",
6415
+ "TransactionInProgressException",
6416
+ "ProvisionedThroughputExceededException",
6417
+ "ThrottlingException",
6418
+ "InternalServerError"
6419
+ ]);
6420
+ /** A missing table reads as one to the schema fence, like SQL's 42P01. */
6421
+ const missingTable = (error) => {
6422
+ throw nameOf(error) === "ResourceNotFoundException" ? Object.assign(new Error(error.message, { cause: error }), { code: "42P01" }) : error;
7908
6423
  };
7909
- const toInsightsInstallationItem = (row) => boundedDynamoDBMetadataItem({
7910
- pk: DYNAMODB_INSIGHTS_INSTALLATIONS_PARTITION,
7911
- sk: row.install_id,
7912
- order_key: insightsSortKey(row),
7913
- version: 1,
7914
- row
7915
- });
7916
- const toInsightsUserItem = (row) => boundedDynamoDBMetadataItem({
7917
- pk: insightsUserPartition(row.user_id),
7918
- sk: row.install_id,
7919
- order_key: insightsSortKey(row),
7920
- version: 1,
7921
- row
7922
- });
7923
- const parseInsightsInstallationItem = (value, partition) => {
7924
- const row = value.row;
7925
- if (value.pk !== partition || typeof value.sk !== "string" || typeof value.order_key !== "string" || value.version !== 1 || !isBundleEventRow(row) || value.sk !== row.install_id || value.order_key !== insightsSortKey(row)) throw new DynamoDBStoredItemError();
6424
+ /**
6425
+ * The key-value store over one DynamoDB table keyed by `pk` and `sk`, with
6426
+ * no secondary index. Reads are strongly consistent; a write is one
6427
+ * `TransactWriteItems` with a client request token.
6428
+ */
6429
+ const createDynamoDBStore = ({ client, tableName, nativePageSize }) => {
6430
+ const documents = _aws_sdk_lib_dynamodb.DynamoDBDocumentClient.from(client, { marshallOptions: { removeUndefinedValues: true } });
7926
6431
  return {
7927
- ...value,
7928
- row
7929
- };
7930
- };
7931
- const loadInsightsInstallationItem = async (store, installId) => {
7932
- const { Item } = await store.client.send(new _aws_sdk_lib_dynamodb.GetCommand({
7933
- TableName: store.tableName,
7934
- Key: {
7935
- pk: DYNAMODB_INSIGHTS_INSTALLATIONS_PARTITION,
7936
- sk: installId
7937
- },
7938
- ConsistentRead: true
7939
- }));
7940
- return Item === void 0 ? null : parseInsightsInstallationItem(Item, DYNAMODB_INSIGHTS_INSTALLATIONS_PARTITION);
7941
- };
7942
- const advancesInsightsInstallation = (row, current) => row.received_at_ms > current.received_at_ms || row.received_at_ms === current.received_at_ms && row.id > current.id;
7943
- const recordDynamoDBInsightsEvent = async (store, { event: row }) => {
7944
- const next = row;
7945
- const eventItem = toInsightsEventItem(row);
7946
- const bundleItem = toInsightsBundleItem(row);
7947
- const identityKey = {
7948
- pk: DYNAMODB_INSIGHTS_EVENT_IDS_PARTITION,
7949
- sk: row.id
7950
- };
7951
- for (let attempt = 0; attempt < DYNAMODB_INSIGHTS_RECORD_ATTEMPTS; attempt++) {
7952
- const { Item: accepted } = await store.client.send(new _aws_sdk_lib_dynamodb.GetCommand({
7953
- TableName: store.tableName,
7954
- Key: identityKey,
7955
- ConsistentRead: true
7956
- }));
7957
- if (accepted !== void 0) return;
7958
- const current = await loadInsightsInstallationItem(store, row.install_id);
7959
- const actions = [
7960
- { Put: {
7961
- TableName: store.tableName,
7962
- Item: identityKey,
7963
- ConditionExpression: "attribute_not_exists(#pk)",
7964
- ExpressionAttributeNames: { "#pk": "pk" }
7965
- } },
7966
- { Put: {
7967
- TableName: store.tableName,
7968
- Item: eventItem
7969
- } },
7970
- { Put: {
7971
- TableName: store.tableName,
7972
- Item: bundleItem
7973
- } }
7974
- ];
7975
- if (current === null || advancesInsightsInstallation(next, current.row)) {
7976
- actions.push({ Put: {
7977
- TableName: store.tableName,
7978
- Item: toInsightsInstallationItem(next),
7979
- ConditionExpression: current === null ? "attribute_not_exists(#pk)" : "#orderKey = :currentOrderKey",
7980
- ExpressionAttributeNames: current === null ? { "#pk": "pk" } : { "#orderKey": "order_key" },
7981
- ...current === null ? {} : { ExpressionAttributeValues: { ":currentOrderKey": current.order_key } }
7982
- } });
7983
- if (current !== null && insightsScopePartition(current.row) !== insightsScopePartition(next)) actions.push({ Delete: {
7984
- TableName: store.tableName,
7985
- Key: {
7986
- pk: insightsScopePartition(current.row),
7987
- sk: current.row.install_id
7988
- }
7989
- } });
7990
- actions.push({ Put: {
7991
- TableName: store.tableName,
7992
- Item: toInsightsScopeItem(next)
7993
- } });
7994
- if (current !== null && current.row.user_id !== null && current.row.user_id !== next.user_id) actions.push({ Delete: {
7995
- TableName: store.tableName,
7996
- Key: {
7997
- pk: insightsUserPartition(current.row.user_id),
7998
- sk: current.row.install_id
6432
+ id: "dynamoDB",
6433
+ limits: DYNAMODB_LIMITS,
6434
+ async get(keys) {
6435
+ const found = /* @__PURE__ */ new Map();
6436
+ const unique = [...new Map(keys.map((key) => [idOf(key), key])).values()];
6437
+ for (let at = 0; at < unique.length; at += GET_BATCH) {
6438
+ let pending = unique.slice(at, at + GET_BATCH).map(({ pk, sk }) => ({
6439
+ pk,
6440
+ sk
6441
+ }));
6442
+ for (let round = 0; pending.length > 0; round += 1) {
6443
+ if (round > 0) await sleep(2 ** round * 10);
6444
+ const result = await documents.send(new _aws_sdk_lib_dynamodb.BatchGetCommand({ RequestItems: { [tableName]: {
6445
+ Keys: pending,
6446
+ ConsistentRead: true
6447
+ } } })).catch(missingTable);
6448
+ for (const item of result.Responses?.[tableName] ?? []) found.set(idOf(item), toRow(item));
6449
+ pending = result.UnprocessedKeys?.[tableName]?.Keys ?? [];
7999
6450
  }
8000
- } });
8001
- if (next.user_id !== null) actions.push({ Put: {
8002
- TableName: store.tableName,
8003
- Item: toInsightsUserItem({
8004
- ...next,
8005
- user_id: next.user_id
8006
- })
8007
- } });
8008
- }
8009
- try {
8010
- await commitDynamoDBTransaction(store, actions);
8011
- return;
8012
- } catch (error) {
8013
- if (!isDynamoDBTransactionConflict(error)) throw error;
8014
- const { Item } = await store.client.send(new _aws_sdk_lib_dynamodb.GetCommand({
8015
- TableName: store.tableName,
8016
- Key: identityKey,
8017
- ConsistentRead: true
8018
- }));
8019
- if (Item !== void 0) return;
8020
- if (attempt === 2) throw error;
8021
- }
8022
- }
8023
- };
8024
- const insightsEventRange = (input) => {
8025
- const upperExclusive = input.after ? `${timestampSortKey(input.after.receivedAtMs)}#${input.after.id}` : `${timestampSortKey(input.beforeReceivedAtMs)}#`;
8026
- const upper = `${upperExclusive.slice(0, -1)}${String.fromCharCode(upperExclusive.charCodeAt(upperExclusive.length - 1) - 1)}~`;
8027
- const movement = input.filter.kind === "installationMovement";
8028
- const partition = movement ? insightsMovementPartition(input.filter.installId) : input.filter.kind === "bundle" ? insightsBundlePartition(input.filter) : DYNAMODB_INSIGHTS_PARTITION;
8029
- return {
8030
- partition,
8031
- query: {
8032
- ...movement ? { IndexName: DYNAMODB_UPDATE_INDEX_NAME } : { ConsistentRead: true },
8033
- KeyConditionExpression: "#partition = :partition AND #order BETWEEN :since AND :upper",
8034
- ExpressionAttributeNames: {
8035
- "#partition": movement ? "gsi1pk" : "pk",
8036
- "#order": movement ? "gsi1sk" : "sk"
8037
- },
8038
- ExpressionAttributeValues: {
8039
- ":partition": partition,
8040
- ":since": `${timestampSortKey(input.sinceMs ?? 0)}#`,
8041
- ":upper": upper
8042
6451
  }
8043
- }
8044
- };
8045
- };
8046
- const createDynamoDBInsightsTable = (store) => ({
8047
- recordEvent: (input) => recordDynamoDBInsightsEvent(store, input),
8048
- async listEvents(input) {
8049
- if ((input.sinceMs ?? 0) === input.beforeReceivedAtMs) return [];
8050
- const range = insightsEventRange(input);
8051
- const rows = [];
8052
- let exclusiveStartKey;
8053
- do {
8054
- const page = await store.client.send(new _aws_sdk_lib_dynamodb.QueryCommand({
8055
- TableName: store.tableName,
8056
- ...range.query,
8057
- ExclusiveStartKey: exclusiveStartKey,
8058
- Limit: input.limit - rows.length,
8059
- ScanIndexForward: false
8060
- }));
8061
- rows.push(...(page.Items ?? []).slice(0, input.limit - rows.length).map((item) => parseOfficialRowItem(item, input.filter.kind === "bundle" ? range.partition : DYNAMODB_INSIGHTS_PARTITION, isBundleEventRow).row));
8062
- exclusiveStartKey = page.LastEvaluatedKey;
8063
- } while (rows.length < input.limit && exclusiveStartKey !== void 0);
8064
- return rows;
8065
- },
8066
- async findLatestEvents(input) {
8067
- if ("installId" in input) {
8068
- const stored = await loadInsightsInstallationItem(store, input.installId);
8069
- return stored === null ? [] : [stored.row];
8070
- }
8071
- const partition = insightsUserPartition(input.userId);
8072
- const rows = [];
8073
- let exclusiveStartKey;
8074
- do {
8075
- const page = await store.client.send(new _aws_sdk_lib_dynamodb.QueryCommand({
8076
- TableName: store.tableName,
8077
- ConsistentRead: true,
8078
- ExclusiveStartKey: exclusiveStartKey,
8079
- KeyConditionExpression: input.afterInstallId === void 0 ? "#pk = :pk" : "#pk = :pk AND #sk > :after",
8080
- ExpressionAttributeNames: {
8081
- "#pk": "pk",
8082
- ...input.afterInstallId === void 0 ? {} : { "#sk": "sk" }
8083
- },
6452
+ return keys.map((key) => found.get(idOf(key)) ?? null);
6453
+ },
6454
+ async query({ pk, gte, lt, lte, order, limit, after }) {
6455
+ let condition = "pk = :pk";
6456
+ if (gte !== void 0 && lt !== void 0) condition += " AND sk BETWEEN :gte AND :lte";
6457
+ else if (gte !== void 0) condition += " AND sk >= :gte";
6458
+ else if (lt !== void 0) condition += " AND sk < :lt";
6459
+ const result = await documents.send(new _aws_sdk_lib_dynamodb.QueryCommand({
6460
+ TableName: tableName,
6461
+ KeyConditionExpression: condition,
8084
6462
  ExpressionAttributeValues: {
8085
- ":pk": partition,
8086
- ...input.afterInstallId === void 0 ? {} : { ":after": input.afterInstallId }
6463
+ ":pk": pk,
6464
+ ":gte": gte,
6465
+ ...gte !== void 0 && lt !== void 0 ? { ":lte": lte ?? lt } : { ":lt": lt }
8087
6466
  },
8088
- Limit: input.limit - rows.length,
8089
- ScanIndexForward: true
8090
- }));
8091
- for (const item of (page.Items ?? []).slice(0, input.limit - rows.length)) {
8092
- const stored = parseInsightsInstallationItem(item, partition);
8093
- const current = await loadInsightsInstallationItem(store, stored.row.install_id);
8094
- if (current?.row.user_id === input.userId) rows.push(current.row);
8095
- }
8096
- exclusiveStartKey = page.LastEvaluatedKey;
8097
- } while (rows.length < input.limit && exclusiveStartKey !== void 0);
8098
- return rows;
8099
- },
8100
- async countLatestEvents(input) {
8101
- const bundleConditions = input.bundle?.map((bundle, group) => `#bundle${group} = :bundle${group} AND #type IN (${bundle.types.map((_, i) => `:type${group}_${i}`).join(", ")})`);
8102
- const bundleFilter = bundleConditions === void 0 ? "" : ` AND ${bundleConditions.length === 1 ? bundleConditions[0] : `(${bundleConditions.join(" OR ")})`}`;
8103
- let count = 0;
8104
- let exclusiveStartKey;
8105
- do {
8106
- const page = await store.client.send(new _aws_sdk_lib_dynamodb.QueryCommand({
8107
- TableName: store.tableName,
6467
+ ScanIndexForward: order === "asc",
8108
6468
  ConsistentRead: true,
8109
- ExclusiveStartKey: exclusiveStartKey,
8110
- KeyConditionExpression: "#pk = :pk",
8111
- FilterExpression: "#received >= :since" + bundleFilter,
8112
- ExpressionAttributeNames: {
8113
- "#pk": "pk",
8114
- "#received": "received_at_ms",
8115
- ...input.bundle === void 0 ? {} : {
8116
- ...Object.fromEntries(input.bundle.map((bundle, group) => [`#bundle${group}`, bundle.field])),
8117
- "#type": "type"
8118
- }
8119
- },
8120
- ExpressionAttributeValues: {
8121
- ":pk": insightsScopePartition(input),
8122
- ":since": input.sinceMs,
8123
- ...input.bundle === void 0 ? {} : Object.fromEntries(input.bundle.flatMap((bundle, group) => [[`:bundle${group}`, bundle.value], ...bundle.types.map((type, i) => [`:type${group}_${i}`, type])]))
8124
- },
8125
- Select: "COUNT",
8126
- ScanIndexForward: true
8127
- }));
8128
- count += page.Count ?? 0;
8129
- exclusiveStartKey = page.LastEvaluatedKey;
8130
- } while (exclusiveStartKey !== void 0);
8131
- return count;
8132
- },
8133
- async countEvents(input) {
8134
- if (input.sinceMs === input.beforeReceivedAtMs) return 0;
8135
- const { query } = insightsEventRange({
8136
- ...input,
8137
- filter: {
8138
- kind: "bundle",
8139
- ...input.filter
8140
- },
8141
- limit: 1
8142
- });
8143
- let count = 0;
8144
- let exclusiveStartKey;
8145
- do {
8146
- const page = await store.client.send(new _aws_sdk_lib_dynamodb.QueryCommand({
8147
- TableName: store.tableName,
8148
- ...query,
8149
- ExclusiveStartKey: exclusiveStartKey,
8150
- Select: "COUNT"
8151
- }));
8152
- count += page.Count ?? 0;
8153
- exclusiveStartKey = page.LastEvaluatedKey;
8154
- } while (exclusiveStartKey !== void 0);
8155
- return count;
8156
- }
8157
- });
8158
- const apiKeyItem = (row, version) => ({
8159
- pk: DYNAMODB_API_KEY_PARTITION,
8160
- sk: row.id,
8161
- version,
8162
- row
8163
- });
8164
- const loadApiKey = async (store, id) => {
8165
- const result = await store.client.send(new _aws_sdk_lib_dynamodb.GetCommand({
8166
- TableName: store.tableName,
8167
- Key: {
8168
- pk: DYNAMODB_API_KEY_PARTITION,
8169
- sk: id
6469
+ Limit: Math.min(limit, nativePageSize ?? limit),
6470
+ ExclusiveStartKey: after === void 0 ? void 0 : {
6471
+ pk,
6472
+ sk: after
6473
+ }
6474
+ })).catch(missingTable);
6475
+ return {
6476
+ items: (result.Items ?? []).map((item) => ({
6477
+ sk: item.sk,
6478
+ value: toRow(item)
6479
+ })),
6480
+ more: result.LastEvaluatedKey !== void 0
6481
+ };
8170
6482
  },
8171
- ConsistentRead: true
8172
- }));
8173
- return result.Item === void 0 ? null : parseOfficialRowItem(result.Item, DYNAMODB_API_KEY_PARTITION, isApiKeyRow);
8174
- };
8175
- const createDynamoDBApiKeyTable = (store) => {
8176
- const findByHash = async (hash) => {
8177
- const lookup = await store.client.send(new _aws_sdk_lib_dynamodb.GetCommand({
8178
- TableName: store.tableName,
8179
- Key: {
8180
- pk: DYNAMODB_API_KEY_HASH_PARTITION,
8181
- sk: hash
8182
- },
8183
- ConsistentRead: true
8184
- }));
8185
- const id = lookup.Item?.api_key_id;
8186
- if (lookup.Item === void 0) return null;
8187
- if (typeof id !== "string") throw new DynamoDBStoredItemError();
8188
- return (await loadApiKey(store, id))?.row ?? null;
8189
- };
8190
- return {
8191
- async create(row) {
8192
- if (await findByHash(row.hash) !== null) return "existing";
6483
+ async write(ops) {
8193
6484
  try {
8194
- await commitDynamoDBTransaction(store, [{ Put: {
8195
- TableName: store.tableName,
8196
- Item: boundedDynamoDBMetadataItem(apiKeyItem(row, 1)),
8197
- ConditionExpression: "attribute_not_exists(#pk)",
8198
- ExpressionAttributeNames: { "#pk": "pk" }
8199
- } }, { Put: {
8200
- TableName: store.tableName,
8201
- Item: {
8202
- pk: DYNAMODB_API_KEY_HASH_PARTITION,
8203
- sk: row.hash,
8204
- api_key_id: row.id
8205
- },
8206
- ConditionExpression: "attribute_not_exists(#pk)",
8207
- ExpressionAttributeNames: { "#pk": "pk" }
8208
- } }]);
8209
- return "created";
6485
+ await documents.send(new _aws_sdk_lib_dynamodb.TransactWriteCommand({
6486
+ TransactItems: ops.map((op) => toTransactItem(tableName, op)),
6487
+ ClientRequestToken: crypto.randomUUID()
6488
+ }));
6489
+ return { ok: true };
8210
6490
  } catch (error) {
8211
- if (await findByHash(row.hash) !== null) return "existing";
8212
- throw error;
6491
+ const { CancellationReasons = [] } = error;
6492
+ const codes = CancellationReasons.map(({ Code }) => Code);
6493
+ const failed = codes.indexOf("ConditionalCheckFailed");
6494
+ if (failed !== -1) return {
6495
+ ok: false,
6496
+ failedOp: failed
6497
+ };
6498
+ return [nameOf(error), ...codes].some((code) => TRANSIENT.has(code)) ? {
6499
+ ok: false,
6500
+ retry: true
6501
+ } : missingTable(error);
8213
6502
  }
8214
6503
  },
8215
- findByHash,
8216
- async list() {
8217
- const rows = [];
8218
- let exclusiveStartKey;
8219
- do {
8220
- const page = await store.client.send(new _aws_sdk_lib_dynamodb.QueryCommand({
8221
- TableName: store.tableName,
8222
- ExclusiveStartKey: exclusiveStartKey,
8223
- KeyConditionExpression: "#pk = :pk",
8224
- ExpressionAttributeNames: { "#pk": "pk" },
8225
- ExpressionAttributeValues: { ":pk": DYNAMODB_API_KEY_PARTITION },
8226
- ConsistentRead: true
8227
- }));
8228
- rows.push(...(page.Items ?? []).map((item) => parseOfficialRowItem(item, DYNAMODB_API_KEY_PARTITION, isApiKeyRow).row));
8229
- exclusiveStartKey = page.LastEvaluatedKey;
8230
- } while (exclusiveStartKey !== void 0);
8231
- return rows.sort((left, right) => right.created_at_ms - left.created_at_ms || left.id.localeCompare(right.id));
8232
- },
8233
- async revoke({ id, revokedAtMs }) {
8234
- const current = await loadApiKey(store, id);
8235
- if (current === null) return null;
8236
- const row = {
8237
- ...current.row,
8238
- revoked_at_ms: revokedAtMs
8239
- };
8240
- await store.client.send(new _aws_sdk_lib_dynamodb.PutCommand({
8241
- TableName: store.tableName,
8242
- Item: boundedDynamoDBMetadataItem(apiKeyItem(row, current.version + 1)),
8243
- ConditionExpression: "#version = :currentVersion",
8244
- ExpressionAttributeNames: { "#version": "version" },
8245
- ExpressionAttributeValues: { ":currentVersion": current.version }
6504
+ migrations: {
6505
+ /** Creates the table for local and test runs; `hot-updater init` creates it in AWS. */
6506
+ async apply() {
6507
+ try {
6508
+ await client.send(new _aws_sdk_client_dynamodb.DescribeTableCommand({ TableName: tableName }));
6509
+ return;
6510
+ } catch (error) {
6511
+ if (nameOf(error) !== "ResourceNotFoundException") throw error;
6512
+ }
6513
+ await client.send(new _aws_sdk_client_dynamodb.CreateTableCommand({
6514
+ TableName: tableName,
6515
+ AttributeDefinitions: [{
6516
+ AttributeName: "pk",
6517
+ AttributeType: "S"
6518
+ }, {
6519
+ AttributeName: "sk",
6520
+ AttributeType: "S"
6521
+ }],
6522
+ KeySchema: [{
6523
+ AttributeName: "pk",
6524
+ KeyType: "HASH"
6525
+ }, {
6526
+ AttributeName: "sk",
6527
+ KeyType: "RANGE"
6528
+ }],
6529
+ BillingMode: "PAY_PER_REQUEST"
8246
6530
  }));
8247
- return row;
8248
- }
6531
+ await (0, _aws_sdk_client_dynamodb.waitUntilTableExists)({
6532
+ client,
6533
+ maxWaitTime: 120,
6534
+ minDelay: 1,
6535
+ maxDelay: 1
6536
+ }, { TableName: tableName });
6537
+ } }
8249
6538
  };
8250
6539
  };
8251
- const DYNAMODB_UPDATE_INDEX_NAME = "hot-updater-update-index";
8252
- const dynamoDB = (config) => {
8253
- const { apiBasePath = "/release-catalogs", cloudfrontDistributionId, shouldWaitForInvalidation = false, tableName, ...clientConfig } = config;
8254
- const client = _aws_sdk_lib_dynamodb.DynamoDBDocumentClient.from(new _aws_sdk_client_dynamodb.DynamoDBClient(clientConfig), { marshallOptions: { removeUndefinedValues: true } });
8255
- const cloudFront = cloudfrontDistributionId ? new _aws_sdk_client_cloudfront.CloudFrontClient({
8256
- credentials: clientConfig.credentials,
8257
- region: clientConfig.region
8258
- }) : null;
8259
- const store = {
6540
+ //#endregion
6541
+ //#region src/dynamoDB.ts
6542
+ /** The DynamoDB client ignores the CloudFront settings in its config. */
6543
+ const adapterOf = ({ tableName, ...clientConfig }) => {
6544
+ const client = new _aws_sdk_client_dynamodb.DynamoDBClient(clientConfig);
6545
+ return {
8260
6546
  client,
8261
- tableName
8262
- };
8263
- const crud = createDynamoDBCrud(store, DYNAMODB_UPDATE_INDEX_NAME);
8264
- const invalidateUpdateRoutes = async () => {
8265
- if (!cloudFront || !cloudfrontDistributionId) return;
8266
- try {
8267
- await invalidateCloudFront(cloudFront, cloudfrontDistributionId, [`${apiBasePath.replace(/\/+$/, "")}/*`], { shouldWait: shouldWaitForInvalidation });
8268
- } catch (error) {
8269
- console.warn("[hot-updater/aws] CloudFront invalidation failed; continuing without cache invalidation.", {
8270
- distributionId: cloudfrontDistributionId,
8271
- error: error instanceof Error ? error.message : "Unknown error"
8272
- });
8273
- }
6547
+ adapter: (0, _hot_updater_server_database.createKvAdapter)({ store: createDynamoDBStore({
6548
+ client,
6549
+ tableName
6550
+ }) })
8274
6551
  };
8275
- const adapter = (0, _hot_updater_plugin_core_internal.createDatabasePluginAdapter)("dynamoDB", {
8276
- ...crud,
8277
- commit: createDynamoDBCommit(store),
8278
- dispose: async () => {
6552
+ };
6553
+ /**
6554
+ * Creates the table when it is missing and writes the schema settings the
6555
+ * database checks before its first read. `hot-updater init` runs it in AWS.
6556
+ */
6557
+ const migrateDynamoDB = async (config) => {
6558
+ const { client, adapter } = adapterOf(config);
6559
+ try {
6560
+ await (0, _hot_updater_server_database.migrateBuiltInSchema)(adapter, "dynamoDB");
6561
+ } finally {
6562
+ client.destroy();
6563
+ }
6564
+ };
6565
+ /**
6566
+ * Hot Updater's database on one DynamoDB table, through the storage engine.
6567
+ * With a distribution, it gives core the CloudFront invalidation core runs
6568
+ * after a write that changes what the update-check routes answer.
6569
+ */
6570
+ const dynamoDB = (config) => {
6571
+ const { client, adapter } = adapterOf(config);
6572
+ const cloudFront = createUpdateRouteInvalidation(config);
6573
+ return {
6574
+ ...(0, _hot_updater_server_database.createEngineDatabase)({
6575
+ name: "dynamoDB",
6576
+ adapter,
6577
+ onCachedRoutesChange: cloudFront?.invalidate
6578
+ }),
6579
+ async dispose() {
6580
+ await adapter.dispose?.();
8279
6581
  client.destroy();
8280
6582
  cloudFront?.destroy();
8281
6583
  }
8282
- });
8283
- return (0, _hot_updater_plugin_core.createDatabasePlugin)({
8284
- name: "dynamoDB",
8285
- models: {
8286
- ...adapter.models,
8287
- bundlePatches: createDynamoDBBundlePatchTable(store, DYNAMODB_UPDATE_INDEX_NAME),
8288
- insights: createDynamoDBInsightsTable(store),
8289
- apiKeys: createDynamoDBApiKeyTable(store)
8290
- },
8291
- async commit(input) {
8292
- const result = await adapter.commit(input);
8293
- if (result.committed && input.changes.some(({ model }) => model === "bundles" || model === "bundlePatches")) await invalidateUpdateRoutes();
8294
- return result;
8295
- },
8296
- dispose: adapter.dispose
8297
- });
6584
+ };
8298
6585
  };
8299
6586
  //#endregion
6587
+ //#region src/plugins.ts
6588
+ /**
6589
+ * The plugins the managed AWS server runs: Insights, and API keys on every
6590
+ * client route. Its Lambda@Edge function imports this list.
6591
+ */
6592
+ const plugins = [(0, _hot_updater_server_plugins_insights.insights)(), (0, _hot_updater_server_plugins_api_keys.apiKeys)()];
6593
+ //#endregion
8300
6594
  //#region iac/init/index.ts
8301
6595
  const AWS_AUTH_MODES = [
8302
6596
  "local-session",
@@ -9185,7 +7479,7 @@ var CloudFrontManager = class {
9185
7479
  return policyId;
9186
7480
  }
9187
7481
  async getOrCreateKeyGroup(publicKey) {
9188
- const publicKeyHash = crypto.default.createHash("sha256").update(publicKey).digest("hex").slice(0, 16);
7482
+ const publicKeyHash = crypto$1.default.createHash("sha256").update(publicKey).digest("hex").slice(0, 16);
9189
7483
  const cloudfrontClient = new _aws_sdk_client_cloudfront.CloudFront({
9190
7484
  region: this.region,
9191
7485
  credentials: this.credentials
@@ -9413,7 +7707,7 @@ var DynamoDBTableSchemaError = class extends Error {
9413
7707
  tableName;
9414
7708
  name = "DynamoDBTableSchemaError";
9415
7709
  constructor(tableName) {
9416
- super(`DynamoDB table "${tableName}" does not match the Hot Updater key and index schema`);
7710
+ super(`DynamoDB table "${tableName}" does not match the Hot Updater schema: a pk and sk key and no secondary index. A table from before 1.0 keeps its update index; delete it and rerun init to create it again.`);
9417
7711
  this.tableName = tableName;
9418
7712
  }
9419
7713
  };
@@ -9442,60 +7736,41 @@ const primaryKeySchema = [{
9442
7736
  AttributeName: "sk",
9443
7737
  KeyType: "RANGE"
9444
7738
  }];
9445
- const updateIndexKeySchema = [{
9446
- AttributeName: "gsi1pk",
9447
- KeyType: "HASH"
9448
- }, {
9449
- AttributeName: "gsi1sk",
9450
- KeyType: "RANGE"
9451
- }];
9452
- const keyAttributes = [
9453
- "pk",
9454
- "sk",
9455
- "gsi1pk",
9456
- "gsi1sk"
9457
- ];
7739
+ const keyAttributes = ["pk", "sk"];
9458
7740
  const onDemandThroughput = {
9459
7741
  MaxReadRequestUnits: 4e3,
9460
7742
  MaxWriteRequestUnits: 100
9461
7743
  };
9462
7744
  const hasKeySchema = (actual, expected) => actual?.length === expected.length && expected.every(({ AttributeName, KeyType }) => actual.some((key) => key.AttributeName === AttributeName && key.KeyType === KeyType));
9463
7745
  const hasExpectedSchema = (table) => {
9464
- const updateIndex = table?.GlobalSecondaryIndexes?.find(({ IndexName }) => IndexName === DYNAMODB_UPDATE_INDEX_NAME);
9465
- return keyAttributes.every((attributeName) => table?.AttributeDefinitions?.some(({ AttributeName, AttributeType }) => AttributeName === attributeName && AttributeType === "S")) && table?.BillingModeSummary?.BillingMode === "PAY_PER_REQUEST" && table.OnDemandThroughput?.MaxReadRequestUnits === onDemandThroughput.MaxReadRequestUnits && table.OnDemandThroughput?.MaxWriteRequestUnits === onDemandThroughput.MaxWriteRequestUnits && hasKeySchema(table?.KeySchema, primaryKeySchema) && hasKeySchema(updateIndex?.KeySchema, updateIndexKeySchema) && updateIndex?.OnDemandThroughput?.MaxReadRequestUnits === onDemandThroughput.MaxReadRequestUnits && updateIndex?.OnDemandThroughput?.MaxWriteRequestUnits === onDemandThroughput.MaxWriteRequestUnits && updateIndex?.Projection?.ProjectionType === "ALL";
7746
+ return keyAttributes.every((attributeName) => table?.AttributeDefinitions?.some(({ AttributeName, AttributeType }) => AttributeName === attributeName && AttributeType === "S")) && table?.BillingModeSummary?.BillingMode === "PAY_PER_REQUEST" && table.OnDemandThroughput?.MaxReadRequestUnits === onDemandThroughput.MaxReadRequestUnits && table.OnDemandThroughput?.MaxWriteRequestUnits === onDemandThroughput.MaxWriteRequestUnits && hasKeySchema(table?.KeySchema, primaryKeySchema) && (table?.GlobalSecondaryIndexes ?? []).length === 0;
9466
7747
  };
9467
7748
  const isResourceNotFound = (error) => typeof error === "object" && error !== null && Reflect.get(error, "name") === "ResourceNotFoundException";
9468
7749
  const buildDynamoDBCreateTableInput = (tableName) => ({
9469
- AttributeDefinitions: [
9470
- {
9471
- AttributeName: "pk",
9472
- AttributeType: "S"
9473
- },
9474
- {
9475
- AttributeName: "sk",
9476
- AttributeType: "S"
9477
- },
9478
- {
9479
- AttributeName: "gsi1pk",
9480
- AttributeType: "S"
9481
- },
9482
- {
9483
- AttributeName: "gsi1sk",
9484
- AttributeType: "S"
9485
- }
9486
- ],
7750
+ AttributeDefinitions: [{
7751
+ AttributeName: "pk",
7752
+ AttributeType: "S"
7753
+ }, {
7754
+ AttributeName: "sk",
7755
+ AttributeType: "S"
7756
+ }],
9487
7757
  BillingMode: "PAY_PER_REQUEST",
9488
7758
  DeletionProtectionEnabled: true,
9489
- GlobalSecondaryIndexes: [{
9490
- IndexName: DYNAMODB_UPDATE_INDEX_NAME,
9491
- KeySchema: [...updateIndexKeySchema],
9492
- Projection: { ProjectionType: "ALL" },
9493
- OnDemandThroughput: onDemandThroughput
9494
- }],
9495
7759
  KeySchema: [...primaryKeySchema],
9496
7760
  OnDemandThroughput: onDemandThroughput,
9497
7761
  TableName: tableName
9498
7762
  });
7763
+ /**
7764
+ * The schema settings items the plugin checks before its first read, as one
7765
+ * BatchWriteItem request: what `migrateDynamoDB` writes to a new table.
7766
+ */
7767
+ const buildDynamoDBSchemaSettingsInput = (tableName) => ({ RequestItems: { [tableName]: Object.entries(_hot_updater_server_database.builtInSettings).map(([key, value]) => ({ PutRequest: { Item: {
7768
+ pk: { S: _hot_updater_server_database.SETTINGS_TABLE.name },
7769
+ sk: { S: (0, _hot_updater_server_database.encodeKvKey)([key]) },
7770
+ key: { S: key },
7771
+ value: { S: value },
7772
+ _v: { N: "0" }
7773
+ } } })) } });
9499
7774
  const buildDynamoDBBackupInput = (tableName) => ({
9500
7775
  PointInTimeRecoverySpecification: { PointInTimeRecoveryEnabled: true },
9501
7776
  TableName: tableName
@@ -9535,17 +7810,16 @@ var DynamoDBManager = class {
9535
7810
  };
9536
7811
  //#endregion
9537
7812
  //#region iac/iam.ts
7813
+ /** The partitions the plugin's items use: each table's rows, and its index items after `#`. */
7814
+ const dynamoDBLeadingKeys = () => [..._hot_updater_server_database.builtInSchema.tables, _hot_updater_server_database.SETTINGS_TABLE].flatMap(({ name }) => [name, `${name}#*`]);
9538
7815
  const buildDynamoDBPolicy = (region, accountId, tableName) => {
9539
7816
  const tableArn = `arn:aws:dynamodb:${region}:${accountId}:table/${tableName}`;
9540
7817
  return {
9541
7818
  Version: "2012-10-17",
9542
7819
  Statement: [{
9543
- Action: ["dynamodb:Query"],
9544
- Effect: "Allow",
9545
- Resource: [`${tableArn}/index/${DYNAMODB_UPDATE_INDEX_NAME}`]
9546
- }, {
9547
7820
  Action: [
9548
7821
  "dynamodb:BatchGetItem",
7822
+ "dynamodb:ConditionCheckItem",
9549
7823
  "dynamodb:DeleteItem",
9550
7824
  "dynamodb:GetItem",
9551
7825
  "dynamodb:PutItem",
@@ -9553,24 +7827,7 @@ const buildDynamoDBPolicy = (region, accountId, tableName) => {
9553
7827
  "dynamodb:TransactWriteItems",
9554
7828
  "dynamodb:UpdateItem"
9555
7829
  ],
9556
- Condition: { "ForAllValues:StringLike": { "dynamodb:LeadingKeys": [
9557
- "_hot-updater",
9558
- "bundles",
9559
- "bundle_patches",
9560
- "release-scope#*",
9561
- "release_catalogs",
9562
- DYNAMODB_RELEASE_ID_PARTITION,
9563
- DYNAMODB_CHANNEL_PARTITION,
9564
- DYNAMODB_CHANNEL_NAME_PARTITION,
9565
- DYNAMODB_INSIGHTS_PARTITION,
9566
- DYNAMODB_INSIGHTS_INSTALLATIONS_PARTITION,
9567
- DYNAMODB_INSIGHTS_EVENT_IDS_PARTITION,
9568
- `${DYNAMODB_INSIGHTS_BUNDLE_PREFIX}*`,
9569
- "_hot-updater#insights-user#*",
9570
- "_hot-updater#insights-scope#*",
9571
- DYNAMODB_API_KEY_PARTITION,
9572
- DYNAMODB_API_KEY_HASH_PARTITION
9573
- ] } },
7830
+ Condition: { "ForAllValues:StringLike": { "dynamodb:LeadingKeys": dynamoDBLeadingKeys() } },
9574
7831
  Effect: "Allow",
9575
7832
  Resource: [tableArn]
9576
7833
  }]
@@ -9964,7 +8221,7 @@ var SSMKeyPairManager = class {
9964
8221
  _hot_updater_cli_tools.p.log.info("Using existing CloudFront key pair from SSM Parameter Store");
9965
8222
  return keyPair;
9966
8223
  }
9967
- const { publicKey, privateKey } = crypto.default.generateKeyPairSync("rsa", {
8224
+ const { publicKey, privateKey } = crypto$1.default.generateKeyPairSync("rsa", {
9968
8225
  modulusLength: 2048,
9969
8226
  publicKeyEncoding: {
9970
8227
  type: "spki",
@@ -9976,7 +8233,7 @@ var SSMKeyPairManager = class {
9976
8233
  }
9977
8234
  });
9978
8235
  const keyPair = {
9979
- keyPairId: `HOTUPDATER-${crypto.default.randomBytes(4).toString("hex").toUpperCase()}`,
8236
+ keyPairId: `HOTUPDATER-${crypto$1.default.randomBytes(4).toString("hex").toUpperCase()}`,
9980
8237
  publicKey,
9981
8238
  privateKey
9982
8239
  };
@@ -10100,10 +8357,11 @@ const isAwsRegion = (value) => {
10100
8357
  };
10101
8358
  const prepareDynamoDBDeployment = async (input) => {
10102
8359
  await new DynamoDBManager(input.region, input.credentials).ensureTable(input.tableName);
8360
+ await migrateDynamoDB(input);
10103
8361
  };
8362
+ /** The app's client key, through the managed server's apiKeys() plugin, on the table it reads. */
10104
8363
  const prepareDynamoDBApiKey = async (input) => {
10105
- return (await (0, _hot_updater_server.provisionApiKey)({
10106
- apiKeys: input.apiKeys,
8364
+ return (await (0, _hot_updater_server_db.createDatabasePluginApis)(input.database, plugins).apiKeys.provision({
10107
8365
  existingApiKey: input.existingApiKey,
10108
8366
  name: "AWS init"
10109
8367
  })).apiKey;
@@ -10267,7 +8525,7 @@ const runInit = async ({ build, envFile }) => {
10267
8525
  let apiKey;
10268
8526
  try {
10269
8527
  apiKey = await prepareDynamoDBApiKey({
10270
- apiKeys: databasePlugin.models.apiKeys,
8528
+ database: databasePlugin,
10271
8529
  existingApiKey: providerEnv.HOT_UPDATER_API_KEY
10272
8530
  });
10273
8531
  await (0, _hot_updater_cli_tools.makeEnv)({ HOT_UPDATER_API_KEY: apiKey });
@@ -10314,6 +8572,7 @@ const runInit = async ({ build, envFile }) => {
10314
8572
  if (configWriteResult.status === "created") _hot_updater_cli_tools.p.log.success("Generated 'hot-updater.config.ts' file with AWS settings.");
10315
8573
  else if (configWriteResult.status === "merged") _hot_updater_cli_tools.p.log.success("Updated 'hot-updater.config.ts' file with AWS settings.");
10316
8574
  else _hot_updater_cli_tools.p.log.warn(`Kept existing 'hot-updater.config.ts' unchanged: ${configWriteResult.reason}`);
8575
+ await (0, _hot_updater_cli_tools.generateHotUpdaterPlugins)("@hot-updater/aws");
10317
8576
  const sourceUrl = `https://${distributionDomain}`;
10318
8577
  _hot_updater_cli_tools.p.note((0, _hot_updater_cli_tools.transformTemplate)(SOURCE_TEMPLATE, {
10319
8578
  apiKey: JSON.stringify(apiKey),
@@ -10329,6 +8588,7 @@ exports.LAMBDA_EDGE_TRUST_POLICY = LAMBDA_EDGE_TRUST_POLICY;
10329
8588
  exports.buildDynamoDBBackupInput = buildDynamoDBBackupInput;
10330
8589
  exports.buildDynamoDBCreateTableInput = buildDynamoDBCreateTableInput;
10331
8590
  exports.buildDynamoDBPolicy = buildDynamoDBPolicy;
8591
+ exports.buildDynamoDBSchemaSettingsInput = buildDynamoDBSchemaSettingsInput;
10332
8592
  exports.buildS3Policy = buildS3Policy;
10333
8593
  exports.buildSsmPolicy = buildSsmPolicy;
10334
8594
  exports.prepareDynamoDBApiKey = prepareDynamoDBApiKey;