@hot-updater/aws 1.0.0-rc.2 → 1.0.0-rc.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +320 -1938
- package/dist/index.d.cts +734 -10
- package/dist/index.d.mts +734 -10
- package/dist/index.mjs +320 -1939
- package/dist/init/index.cjs +8536 -4
- package/dist/init/index.d.cts +124 -3
- package/dist/init/index.d.mts +124 -3
- package/dist/init/index.mjs +8523 -1
- package/dist/lambda/index.cjs +11617 -8579
- package/dist/lambda/index.d.cts +344 -1
- package/package.json +7 -13
- package/dist/iac/index.cjs +0 -10287
- package/dist/iac/index.d.cts +0 -19
- package/dist/iac/index.d.mts +0 -19
- package/dist/iac/index.mjs +0 -10280
package/dist/index.cjs
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
let node_crypto = require("node:crypto");
|
|
3
|
-
let _aws_sdk_client_cloudfront = require("@aws-sdk/client-cloudfront");
|
|
4
2
|
let _aws_sdk_client_dynamodb = require("@aws-sdk/client-dynamodb");
|
|
5
|
-
let _aws_sdk_lib_dynamodb = require("@aws-sdk/lib-dynamodb");
|
|
6
3
|
let _hot_updater_plugin_core = require("@hot-updater/plugin-core");
|
|
7
|
-
let
|
|
4
|
+
let _aws_sdk_client_cloudfront = require("@aws-sdk/client-cloudfront");
|
|
5
|
+
let _aws_sdk_lib_dynamodb = require("@aws-sdk/lib-dynamodb");
|
|
8
6
|
let _aws_sdk_client_ssm = require("@aws-sdk/client-ssm");
|
|
9
7
|
let _aws_sdk_cloudfront_signer = require("@aws-sdk/cloudfront-signer");
|
|
10
8
|
let _aws_sdk_client_s3 = require("@aws-sdk/client-s3");
|
|
11
9
|
let _aws_sdk_lib_storage = require("@aws-sdk/lib-storage");
|
|
10
|
+
let _hot_updater_server_plugins_api_keys = require("@hot-updater/server/plugins/api-keys");
|
|
11
|
+
let _hot_updater_server_plugins_insights = require("@hot-updater/server/plugins/insights");
|
|
12
12
|
//#region src/cloudFrontInvalidation.ts
|
|
13
13
|
const DEFAULT_POLL_INTERVAL_MS = 2e3;
|
|
14
14
|
const DEFAULT_TIMEOUT_MS = 3e5;
|
|
15
|
-
const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
15
|
+
const sleep$1 = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
16
16
|
const invalidateCloudFront = async (client, distributionId, paths, options) => {
|
|
17
17
|
if (paths.length === 0) return;
|
|
18
18
|
const response = await client.send(new _aws_sdk_client_cloudfront.CreateInvalidationCommand({
|
|
@@ -30,7 +30,7 @@ const invalidateCloudFront = async (client, distributionId, paths, options) => {
|
|
|
30
30
|
if (!invalidationId) throw new Error("CloudFront invalidation response is missing Invalidation.Id");
|
|
31
31
|
const deadline = Date.now() + DEFAULT_TIMEOUT_MS;
|
|
32
32
|
while (Date.now() < deadline) {
|
|
33
|
-
await sleep(DEFAULT_POLL_INTERVAL_MS);
|
|
33
|
+
await sleep$1(DEFAULT_POLL_INTERVAL_MS);
|
|
34
34
|
if ((await client.send(new _aws_sdk_client_cloudfront.GetInvalidationCommand({
|
|
35
35
|
DistributionId: distributionId,
|
|
36
36
|
Id: invalidationId
|
|
@@ -38,1971 +38,328 @@ const invalidateCloudFront = async (client, distributionId, paths, options) => {
|
|
|
38
38
|
}
|
|
39
39
|
throw new Error(`Timed out waiting for CloudFront invalidation ${invalidationId}.`);
|
|
40
40
|
};
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
};
|
|
53
|
-
const boundedDynamoDBMetadataItem = (item) => {
|
|
54
|
-
const byteLength = new TextEncoder().encode(JSON.stringify(item)).byteLength;
|
|
55
|
-
if (byteLength > 8192) throw new DynamoDBMetadataItemSizeError(byteLength);
|
|
56
|
-
return item;
|
|
57
|
-
};
|
|
58
|
-
var DynamoDBCatalogItemSizeError = class extends Error {
|
|
59
|
-
byteLength;
|
|
60
|
-
name = "DynamoDBCatalogItemSizeError";
|
|
61
|
-
constructor(byteLength) {
|
|
62
|
-
super(`DynamoDB catalog item is ${byteLength} bytes; maximum is ${DYNAMODB_MAX_CATALOG_ITEM_BYTES}`);
|
|
63
|
-
this.byteLength = byteLength;
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
const boundedDynamoDBCatalogItem = (item) => {
|
|
67
|
-
const byteLength = new TextEncoder().encode(JSON.stringify(item)).byteLength;
|
|
68
|
-
if (byteLength > 409600) throw new DynamoDBCatalogItemSizeError(byteLength);
|
|
69
|
-
return item;
|
|
70
|
-
};
|
|
71
|
-
const DYNAMODB_CHANNEL_PARTITION = "channels";
|
|
72
|
-
const DYNAMODB_CHANNEL_NAME_PARTITION = "_hot-updater#channel-names";
|
|
73
|
-
var DynamoDBStoredItemError = class extends Error {
|
|
74
|
-
name = "DynamoDBStoredItemError";
|
|
75
|
-
constructor() {
|
|
76
|
-
super("DynamoDB contains an invalid Hot Updater row");
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
const field = (value, name) => Reflect.get(value, name);
|
|
80
|
-
const isNullableString = (value) => value === null || typeof value === "string";
|
|
81
|
-
const isByteSize = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
82
|
-
const withLegacyArchiveByteSize = (value) => typeof value === "object" && value !== null && field(value, "archive_byte_size") === void 0 ? {
|
|
83
|
-
...value,
|
|
84
|
-
archive_byte_size: 0
|
|
85
|
-
} : value;
|
|
86
|
-
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"));
|
|
87
|
-
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";
|
|
88
|
-
const isChannelRow = (value) => typeof value === "object" && value !== null && typeof field(value, "id") === "string" && typeof field(value, "name") === "string";
|
|
89
|
-
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";
|
|
90
|
-
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";
|
|
91
|
-
const parseDynamoDBItem = (value) => {
|
|
92
|
-
const pk = value.pk;
|
|
93
|
-
const sk = value.sk;
|
|
94
|
-
const version = value.version;
|
|
95
|
-
const relationCount = value.relation_count;
|
|
96
|
-
const ownedPatchCount = value.owned_patch_count;
|
|
97
|
-
const gsi1pk = value.gsi1pk;
|
|
98
|
-
const gsi1sk = value.gsi1sk;
|
|
99
|
-
const row = pk === "bundles" ? withLegacyArchiveByteSize(value.row) : value.row;
|
|
100
|
-
if (typeof sk !== "string" || typeof version !== "number") throw new DynamoDBStoredItemError();
|
|
101
|
-
if (pk === "channels" && typeof value.reference_count === "number" && isChannelRow(row)) return {
|
|
102
|
-
pk,
|
|
103
|
-
sk,
|
|
104
|
-
version,
|
|
105
|
-
reference_count: value.reference_count,
|
|
106
|
-
row
|
|
107
|
-
};
|
|
108
|
-
if (typeof pk === "string" && pk.startsWith("release-scope#") && isReleaseRow(row)) return {
|
|
109
|
-
pk,
|
|
110
|
-
sk,
|
|
111
|
-
version,
|
|
112
|
-
row
|
|
113
|
-
};
|
|
114
|
-
if (pk === "release_catalogs" && isReleaseCatalogRow(row)) return {
|
|
115
|
-
pk,
|
|
116
|
-
sk,
|
|
117
|
-
version,
|
|
118
|
-
row
|
|
119
|
-
};
|
|
120
|
-
if (pk === "bundles" && isBundleRow(row)) {
|
|
121
|
-
if (typeof relationCount !== "number" || typeof ownedPatchCount !== "number") throw new DynamoDBStoredItemError();
|
|
122
|
-
return {
|
|
123
|
-
pk,
|
|
124
|
-
sk,
|
|
125
|
-
version,
|
|
126
|
-
relation_count: relationCount,
|
|
127
|
-
owned_patch_count: ownedPatchCount,
|
|
128
|
-
row
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
if (pk === "bundle_patches" && typeof gsi1pk === "string" && typeof gsi1sk === "string" && isPatchRow(row)) return {
|
|
132
|
-
pk,
|
|
133
|
-
sk,
|
|
134
|
-
version,
|
|
135
|
-
gsi1pk,
|
|
136
|
-
gsi1sk,
|
|
137
|
-
row
|
|
138
|
-
};
|
|
139
|
-
throw new DynamoDBStoredItemError();
|
|
140
|
-
};
|
|
141
|
-
const patchOwnerPartition = (bundleId) => `patch-owner#${bundleId}`;
|
|
142
|
-
const patchOrderKey = (row) => `${row.order_index.toString().padStart(10, "0")}#${row.id}`;
|
|
143
|
-
const toDynamoDBBundleItem = (row, version = 1, relationCount = 0, ownedPatchCount = 0) => ({
|
|
144
|
-
pk: "bundles",
|
|
145
|
-
sk: row.id,
|
|
146
|
-
version,
|
|
147
|
-
relation_count: relationCount,
|
|
148
|
-
owned_patch_count: ownedPatchCount,
|
|
149
|
-
row
|
|
150
|
-
});
|
|
151
|
-
const toDynamoDBPatchItem = (row, version = 1) => {
|
|
152
|
-
const orderKey = patchOrderKey(row);
|
|
41
|
+
/**
|
|
42
|
+
* The update-check routes' CloudFront invalidation: `invalidate` purges their
|
|
43
|
+
* cached copies and `destroy` releases the client; undefined without a
|
|
44
|
+
* distribution. A failed invalidation only warns, so the write that changed
|
|
45
|
+
* the routes still succeeds.
|
|
46
|
+
*/
|
|
47
|
+
const createUpdateRouteInvalidation = ({ apiBasePath = "/release-catalogs", cloudfrontDistributionId, shouldWaitForInvalidation = false, credentials, region }) => {
|
|
48
|
+
if (!cloudfrontDistributionId) return void 0;
|
|
49
|
+
const client = new _aws_sdk_client_cloudfront.CloudFrontClient({
|
|
50
|
+
credentials,
|
|
51
|
+
region
|
|
52
|
+
});
|
|
153
53
|
return {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
};
|
|
162
|
-
const toDynamoDBChannelItem = (row, version = 1, referenceCount = 0) => ({
|
|
163
|
-
pk: DYNAMODB_CHANNEL_PARTITION,
|
|
164
|
-
sk: row.id,
|
|
165
|
-
version,
|
|
166
|
-
reference_count: referenceCount,
|
|
167
|
-
row
|
|
168
|
-
});
|
|
169
|
-
const releaseScopePartition = (scopeKey) => `release-scope#${scopeKey}`;
|
|
170
|
-
const toDynamoDBReleaseItem = (row, version = 1) => ({
|
|
171
|
-
pk: releaseScopePartition(row.scope_key),
|
|
172
|
-
sk: row.id,
|
|
173
|
-
version,
|
|
174
|
-
row
|
|
175
|
-
});
|
|
176
|
-
const toDynamoDBReleaseCatalogItem = (row, version = 1) => ({
|
|
177
|
-
pk: "release_catalogs",
|
|
178
|
-
sk: row.scope_key,
|
|
179
|
-
version,
|
|
180
|
-
row
|
|
181
|
-
});
|
|
182
|
-
const itemKey = (model, id) => ({
|
|
183
|
-
pk: model,
|
|
184
|
-
sk: id
|
|
185
|
-
});
|
|
186
|
-
const exactDynamoDBId = (where) => {
|
|
187
|
-
if (where?.length !== 1) return void 0;
|
|
188
|
-
const condition = where[0];
|
|
189
|
-
if (Reflect.get(condition, "field") !== "id" || (Reflect.get(condition, "operator") ?? "eq") !== "eq" || Reflect.get(condition, "mode") === "insensitive") return;
|
|
190
|
-
const value = Reflect.get(condition, "value");
|
|
191
|
-
return typeof value === "string" ? value : void 0;
|
|
192
|
-
};
|
|
193
|
-
const exactDynamoDBPatchOwner = (where) => {
|
|
194
|
-
const owners = exactDynamoDBPatchOwners(where);
|
|
195
|
-
return owners?.length === 1 ? owners[0] : void 0;
|
|
196
|
-
};
|
|
197
|
-
const exactDynamoDBPatchOwners = (where) => {
|
|
198
|
-
if (!where || where.some((condition) => Reflect.get(condition, "connector") === "OR")) return;
|
|
199
|
-
const ownerConditions = where.filter((condition) => Reflect.get(condition, "field") === "bundle_id");
|
|
200
|
-
if (ownerConditions.length !== 1) return void 0;
|
|
201
|
-
const condition = ownerConditions[0];
|
|
202
|
-
if (Reflect.get(condition, "mode") === "insensitive") return;
|
|
203
|
-
const operator = Reflect.get(condition, "operator") ?? "eq";
|
|
204
|
-
const value = Reflect.get(condition, "value");
|
|
205
|
-
if (operator === "eq") return typeof value === "string" ? [value] : void 0;
|
|
206
|
-
if (operator === "in" && Array.isArray(value) && value.every((id) => typeof id === "string")) return value;
|
|
207
|
-
};
|
|
208
|
-
const exactDynamoDBBundleIds = (where) => {
|
|
209
|
-
if (!where || where.some((condition) => Reflect.get(condition, "connector") === "OR")) return;
|
|
210
|
-
for (const condition of where) {
|
|
211
|
-
if (Reflect.get(condition, "field") !== "id" || Reflect.get(condition, "mode") === "insensitive") continue;
|
|
212
|
-
const operator = Reflect.get(condition, "operator") ?? "eq";
|
|
213
|
-
const value = Reflect.get(condition, "value");
|
|
214
|
-
if (operator === "eq" && typeof value === "string") return [value];
|
|
215
|
-
if (operator === "in" && Array.isArray(value) && value.every((id) => typeof id === "string")) return value;
|
|
216
|
-
}
|
|
217
|
-
};
|
|
218
|
-
const dynamoDBBundlePageStart = (where, direction) => {
|
|
219
|
-
if ((where ?? []).some((condition) => Reflect.get(condition, "connector") === "OR")) return;
|
|
220
|
-
let result;
|
|
221
|
-
for (const condition of where ?? []) {
|
|
222
|
-
if (Reflect.get(condition, "field") !== "id") continue;
|
|
223
|
-
const operator = Reflect.get(condition, "operator") ?? "eq";
|
|
224
|
-
const value = Reflect.get(condition, "value");
|
|
225
|
-
if (!(direction === "asc" ? operator === "gt" || operator === "gte" : operator === "lt" || operator === "lte") || typeof value !== "string") continue;
|
|
226
|
-
const inclusive = operator === "gte" || operator === "lte";
|
|
227
|
-
if (result === void 0 || (direction === "asc" ? value > result.id : value < result.id) || value === result.id && !inclusive) result = {
|
|
228
|
-
id: value,
|
|
229
|
-
inclusive
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
return result;
|
|
233
|
-
};
|
|
234
|
-
const parseStoredItem = (item) => parseDynamoDBItem(item.pk === "release_catalogs" ? boundedDynamoDBCatalogItem(item) : boundedDynamoDBMetadataItem(item));
|
|
235
|
-
const queryItems = async (store, input) => {
|
|
236
|
-
let exclusiveStartKey;
|
|
237
|
-
const items = [];
|
|
238
|
-
do {
|
|
239
|
-
const sortCondition = input.minimumSortKey ? " AND #sortKey >= :minimumSortKey" : "";
|
|
240
|
-
const page = await store.client.send(new _aws_sdk_lib_dynamodb.QueryCommand({
|
|
241
|
-
TableName: store.tableName,
|
|
242
|
-
...input.indexName ? { IndexName: input.indexName } : {},
|
|
243
|
-
...input.consistentRead ? { ConsistentRead: true } : {},
|
|
244
|
-
ExclusiveStartKey: exclusiveStartKey,
|
|
245
|
-
KeyConditionExpression: `#partitionKey = :partition${sortCondition}`,
|
|
246
|
-
ExpressionAttributeNames: {
|
|
247
|
-
"#partitionKey": input.indexName ? "gsi1pk" : "pk",
|
|
248
|
-
...input.minimumSortKey ? { "#sortKey": input.indexName ? "gsi1sk" : "sk" } : {}
|
|
249
|
-
},
|
|
250
|
-
ExpressionAttributeValues: {
|
|
251
|
-
":partition": input.partition,
|
|
252
|
-
...input.minimumSortKey ? { ":minimumSortKey": input.minimumSortKey } : {}
|
|
54
|
+
invalidate: async () => {
|
|
55
|
+
try {
|
|
56
|
+
await invalidateCloudFront(client, cloudfrontDistributionId, [`${apiBasePath.replace(/\/+$/, "")}/*`], { shouldWait: shouldWaitForInvalidation });
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.warn("[hot-updater/aws] CloudFront invalidation failed; continuing without cache invalidation.", {
|
|
59
|
+
distributionId: cloudfrontDistributionId,
|
|
60
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
61
|
+
});
|
|
253
62
|
}
|
|
254
|
-
}));
|
|
255
|
-
const pageItems = (page.Items ?? []).map(parseStoredItem);
|
|
256
|
-
items.push(...pageItems);
|
|
257
|
-
exclusiveStartKey = page.LastEvaluatedKey;
|
|
258
|
-
} while (exclusiveStartKey !== void 0);
|
|
259
|
-
return items;
|
|
260
|
-
};
|
|
261
|
-
const queryBundleItemsPage = async (store, input) => {
|
|
262
|
-
if (input.limit === 0) return [];
|
|
263
|
-
let exclusiveStartKey;
|
|
264
|
-
const items = [];
|
|
265
|
-
do {
|
|
266
|
-
const startOperator = input.start?.inclusive ? input.direction === "asc" ? ">=" : "<=" : input.direction === "asc" ? ">" : "<";
|
|
267
|
-
const page = await store.client.send(new _aws_sdk_lib_dynamodb.QueryCommand({
|
|
268
|
-
TableName: store.tableName,
|
|
269
|
-
ExclusiveStartKey: exclusiveStartKey,
|
|
270
|
-
KeyConditionExpression: input.start ? `#pk = :pk AND #sk ${startOperator} :start` : "#pk = :pk",
|
|
271
|
-
ExpressionAttributeNames: {
|
|
272
|
-
"#pk": "pk",
|
|
273
|
-
...input.start ? { "#sk": "sk" } : {}
|
|
274
|
-
},
|
|
275
|
-
ExpressionAttributeValues: {
|
|
276
|
-
":pk": "bundles",
|
|
277
|
-
...input.start ? { ":start": input.start.id } : {}
|
|
278
|
-
},
|
|
279
|
-
Limit: Math.max(input.limit - items.length, 100),
|
|
280
|
-
ScanIndexForward: input.direction === "asc"
|
|
281
|
-
}));
|
|
282
|
-
for (const item of (page.Items ?? []).map(parseStoredItem)) {
|
|
283
|
-
if (item.pk === "bundles" && input.matches(item.row)) items.push(item);
|
|
284
|
-
if (items.length === input.limit) return items;
|
|
285
|
-
}
|
|
286
|
-
exclusiveStartKey = page.LastEvaluatedKey;
|
|
287
|
-
} while (exclusiveStartKey !== void 0);
|
|
288
|
-
return items;
|
|
289
|
-
};
|
|
290
|
-
const loadModelItems = (store, model) => queryItems(store, {
|
|
291
|
-
partition: model,
|
|
292
|
-
consistentRead: true
|
|
293
|
-
});
|
|
294
|
-
const loadModelItem = async (store, model, id) => {
|
|
295
|
-
const { Item } = await store.client.send(new _aws_sdk_lib_dynamodb.GetCommand({
|
|
296
|
-
TableName: store.tableName,
|
|
297
|
-
Key: itemKey(model, id),
|
|
298
|
-
ConsistentRead: true
|
|
299
|
-
}));
|
|
300
|
-
return Item === void 0 ? void 0 : parseStoredItem(Item);
|
|
301
|
-
};
|
|
302
|
-
const loadBundleItem = async (store, id) => {
|
|
303
|
-
const item = await loadModelItem(store, "bundles", id);
|
|
304
|
-
return item?.pk === "bundles" ? item : void 0;
|
|
305
|
-
};
|
|
306
|
-
const loadPatchItem = async (store, id) => {
|
|
307
|
-
const item = await loadModelItem(store, "bundle_patches", id);
|
|
308
|
-
return item?.pk === "bundle_patches" ? item : void 0;
|
|
309
|
-
};
|
|
310
|
-
const loadChannelItem = async (store, id) => {
|
|
311
|
-
const item = await loadModelItem(store, DYNAMODB_CHANNEL_PARTITION, id);
|
|
312
|
-
return item?.pk === "channels" ? item : void 0;
|
|
313
|
-
};
|
|
314
|
-
const loadBundleItems = async (store) => {
|
|
315
|
-
return (await loadModelItems(store, "bundles")).filter((item) => item.pk === "bundles");
|
|
316
|
-
};
|
|
317
|
-
const loadPatchItems = async (store) => {
|
|
318
|
-
return (await loadModelItems(store, "bundle_patches")).filter((item) => item.pk === "bundle_patches");
|
|
319
|
-
};
|
|
320
|
-
const loadChannelItems = async (store) => {
|
|
321
|
-
return (await loadModelItems(store, DYNAMODB_CHANNEL_PARTITION)).filter((item) => item.pk === DYNAMODB_CHANNEL_PARTITION);
|
|
322
|
-
};
|
|
323
|
-
const DYNAMODB_RELEASE_ID_PARTITION = "_hot-updater#release-scope-by-id";
|
|
324
|
-
const releaseLocatorItem = (row) => ({
|
|
325
|
-
pk: DYNAMODB_RELEASE_ID_PARTITION,
|
|
326
|
-
sk: row.id,
|
|
327
|
-
scope_key: row.scope_key
|
|
328
|
-
});
|
|
329
|
-
const loadReleaseItem = async (store, id) => {
|
|
330
|
-
const locator = await store.client.send(new _aws_sdk_lib_dynamodb.GetCommand({
|
|
331
|
-
TableName: store.tableName,
|
|
332
|
-
Key: {
|
|
333
|
-
pk: DYNAMODB_RELEASE_ID_PARTITION,
|
|
334
|
-
sk: id
|
|
335
|
-
},
|
|
336
|
-
ConsistentRead: true
|
|
337
|
-
}));
|
|
338
|
-
if (locator.Item === void 0) return void 0;
|
|
339
|
-
const scopeKey = locator.Item.scope_key;
|
|
340
|
-
if (typeof scopeKey !== "string") throw new DynamoDBStoredItemError();
|
|
341
|
-
const result = await store.client.send(new _aws_sdk_lib_dynamodb.GetCommand({
|
|
342
|
-
TableName: store.tableName,
|
|
343
|
-
Key: {
|
|
344
|
-
pk: releaseScopePartition(scopeKey),
|
|
345
|
-
sk: id
|
|
346
|
-
},
|
|
347
|
-
ConsistentRead: true
|
|
348
|
-
}));
|
|
349
|
-
if (result.Item === void 0) throw new DynamoDBStoredItemError();
|
|
350
|
-
const item = parseStoredItem(result.Item);
|
|
351
|
-
if (!isReleaseRow(item.row) || item.sk !== id) throw new DynamoDBStoredItemError();
|
|
352
|
-
return item;
|
|
353
|
-
};
|
|
354
|
-
const loadReleaseItemsByScope = async (store, scopeKey) => (await queryItems(store, {
|
|
355
|
-
partition: releaseScopePartition(scopeKey),
|
|
356
|
-
consistentRead: true
|
|
357
|
-
})).filter((item) => item.pk === releaseScopePartition(scopeKey) && isReleaseRow(item.row));
|
|
358
|
-
const loadReleaseItems = async (store) => {
|
|
359
|
-
let exclusiveStartKey;
|
|
360
|
-
const items = [];
|
|
361
|
-
do {
|
|
362
|
-
const page = await store.client.send(new _aws_sdk_lib_dynamodb.ScanCommand({
|
|
363
|
-
TableName: store.tableName,
|
|
364
|
-
ExclusiveStartKey: exclusiveStartKey,
|
|
365
|
-
ConsistentRead: true,
|
|
366
|
-
FilterExpression: "begins_with(#pk, :prefix)",
|
|
367
|
-
ExpressionAttributeNames: { "#pk": "pk" },
|
|
368
|
-
ExpressionAttributeValues: { ":prefix": "release-scope#" }
|
|
369
|
-
}));
|
|
370
|
-
for (const value of page.Items ?? []) {
|
|
371
|
-
const item = parseStoredItem(value);
|
|
372
|
-
if (isReleaseRow(item.row)) items.push(item);
|
|
373
|
-
}
|
|
374
|
-
exclusiveStartKey = page.LastEvaluatedKey;
|
|
375
|
-
} while (exclusiveStartKey !== void 0);
|
|
376
|
-
return items;
|
|
377
|
-
};
|
|
378
|
-
const loadReleaseCatalogItem = async (store, scopeKey) => {
|
|
379
|
-
const result = await store.client.send(new _aws_sdk_lib_dynamodb.GetCommand({
|
|
380
|
-
TableName: store.tableName,
|
|
381
|
-
Key: itemKey("release_catalogs", scopeKey),
|
|
382
|
-
ConsistentRead: true
|
|
383
|
-
}));
|
|
384
|
-
if (result.Item === void 0) return void 0;
|
|
385
|
-
const item = parseStoredItem(result.Item);
|
|
386
|
-
return item.pk === "release_catalogs" ? item : void 0;
|
|
387
|
-
};
|
|
388
|
-
const loadReleaseCatalogItems = async (store) => (await queryItems(store, {
|
|
389
|
-
partition: "release_catalogs",
|
|
390
|
-
consistentRead: true
|
|
391
|
-
})).filter((item) => item.pk === "release_catalogs");
|
|
392
|
-
const loadChannelByName = async (store, name) => {
|
|
393
|
-
const { Item } = await store.client.send(new _aws_sdk_lib_dynamodb.GetCommand({
|
|
394
|
-
TableName: store.tableName,
|
|
395
|
-
Key: {
|
|
396
|
-
pk: DYNAMODB_CHANNEL_NAME_PARTITION,
|
|
397
|
-
sk: name
|
|
398
63
|
},
|
|
399
|
-
|
|
400
|
-
}));
|
|
401
|
-
const id = Item?.channel_id;
|
|
402
|
-
if (Item === void 0) return void 0;
|
|
403
|
-
if (typeof id !== "string") throw new DynamoDBStoredItemError();
|
|
404
|
-
const channel = await loadChannelItem(store, id);
|
|
405
|
-
if (channel === void 0 || channel.row.name !== name) throw new DynamoDBStoredItemError();
|
|
406
|
-
return channel;
|
|
407
|
-
};
|
|
408
|
-
const insertDynamoDBChannel = async (store, input) => {
|
|
409
|
-
const existing = await loadChannelByName(store, input.row.name);
|
|
410
|
-
if (existing !== void 0) return {
|
|
411
|
-
row: existing.row,
|
|
412
|
-
inserted: false
|
|
413
|
-
};
|
|
414
|
-
const existingId = await loadChannelItem(store, input.row.id);
|
|
415
|
-
if (existingId !== void 0) {
|
|
416
|
-
if (existingId.row.name === input.row.name) return {
|
|
417
|
-
row: existingId.row,
|
|
418
|
-
inserted: false
|
|
419
|
-
};
|
|
420
|
-
throw new DynamoDBStoredItemError();
|
|
421
|
-
}
|
|
422
|
-
try {
|
|
423
|
-
await commitDynamoDBTransaction(store, [{ Put: {
|
|
424
|
-
TableName: store.tableName,
|
|
425
|
-
Item: boundedDynamoDBMetadataItem(toDynamoDBChannelItem(input.row)),
|
|
426
|
-
ConditionExpression: "attribute_not_exists(#pk)",
|
|
427
|
-
ExpressionAttributeNames: { "#pk": "pk" }
|
|
428
|
-
} }, { Put: {
|
|
429
|
-
TableName: store.tableName,
|
|
430
|
-
Item: {
|
|
431
|
-
pk: DYNAMODB_CHANNEL_NAME_PARTITION,
|
|
432
|
-
sk: input.row.name,
|
|
433
|
-
channel_id: input.row.id
|
|
434
|
-
},
|
|
435
|
-
ConditionExpression: "attribute_not_exists(#pk)",
|
|
436
|
-
ExpressionAttributeNames: { "#pk": "pk" }
|
|
437
|
-
} }]);
|
|
438
|
-
return {
|
|
439
|
-
row: input.row,
|
|
440
|
-
inserted: true
|
|
441
|
-
};
|
|
442
|
-
} catch (error) {
|
|
443
|
-
const canonical = await loadChannelByName(store, input.row.name);
|
|
444
|
-
if (canonical !== void 0) return {
|
|
445
|
-
row: canonical.row,
|
|
446
|
-
inserted: false
|
|
447
|
-
};
|
|
448
|
-
throw error;
|
|
449
|
-
}
|
|
450
|
-
};
|
|
451
|
-
const deleteDynamoDBChannel = async (store, input) => {
|
|
452
|
-
const current = await loadChannelItem(store, input.id);
|
|
453
|
-
if (current === void 0) return {
|
|
454
|
-
deleted: false,
|
|
455
|
-
reason: "not_found"
|
|
456
|
-
};
|
|
457
|
-
if (current.reference_count !== 0) return {
|
|
458
|
-
deleted: false,
|
|
459
|
-
reason: "not_empty"
|
|
64
|
+
destroy: () => client.destroy()
|
|
460
65
|
};
|
|
461
|
-
try {
|
|
462
|
-
await commitDynamoDBTransaction(store, [{ Delete: {
|
|
463
|
-
TableName: store.tableName,
|
|
464
|
-
Key: itemKey(DYNAMODB_CHANNEL_PARTITION, current.sk),
|
|
465
|
-
ConditionExpression: "#version = :version AND #referenceCount = :zero",
|
|
466
|
-
ExpressionAttributeNames: {
|
|
467
|
-
"#version": "version",
|
|
468
|
-
"#referenceCount": "reference_count"
|
|
469
|
-
},
|
|
470
|
-
ExpressionAttributeValues: {
|
|
471
|
-
":version": current.version,
|
|
472
|
-
":zero": 0
|
|
473
|
-
}
|
|
474
|
-
} }, { Delete: {
|
|
475
|
-
TableName: store.tableName,
|
|
476
|
-
Key: {
|
|
477
|
-
pk: DYNAMODB_CHANNEL_NAME_PARTITION,
|
|
478
|
-
sk: current.row.name
|
|
479
|
-
},
|
|
480
|
-
ConditionExpression: "#channelId = :channelId",
|
|
481
|
-
ExpressionAttributeNames: { "#channelId": "channel_id" },
|
|
482
|
-
ExpressionAttributeValues: { ":channelId": current.sk }
|
|
483
|
-
} }]);
|
|
484
|
-
return { deleted: true };
|
|
485
|
-
} catch (error) {
|
|
486
|
-
const latest = await loadChannelItem(store, input.id);
|
|
487
|
-
if (latest !== void 0 && latest.reference_count > 0) return {
|
|
488
|
-
deleted: false,
|
|
489
|
-
reason: "not_empty"
|
|
490
|
-
};
|
|
491
|
-
if (latest === void 0) return {
|
|
492
|
-
deleted: false,
|
|
493
|
-
reason: "not_found"
|
|
494
|
-
};
|
|
495
|
-
throw error;
|
|
496
|
-
}
|
|
497
|
-
};
|
|
498
|
-
const queryOwnerPatchIds = async (store, indexName, bundleId) => {
|
|
499
|
-
return (await queryItems(store, {
|
|
500
|
-
partition: patchOwnerPartition(bundleId),
|
|
501
|
-
indexName
|
|
502
|
-
})).filter((item) => item.pk === "bundle_patches").map(({ sk }) => sk);
|
|
503
66
|
};
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region src/dynamoDBStore.ts
|
|
69
|
+
/** DynamoDB's limits: 100 items and 4 MB a transaction, 400 KB an item, and 2,048- and 1,024-byte keys. */
|
|
70
|
+
const DYNAMODB_LIMITS = {
|
|
71
|
+
items: 100,
|
|
72
|
+
bytes: 4e6,
|
|
73
|
+
itemBytes: 4e5,
|
|
74
|
+
keyBytes: {
|
|
75
|
+
pk: 2048,
|
|
76
|
+
sk: 1024
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
/** BatchGetItem takes 100 keys a call, and BatchWriteItem 25 requests. */
|
|
80
|
+
const GET_BATCH = 100;
|
|
81
|
+
const WRITE_BATCH = 25;
|
|
82
|
+
/**
|
|
83
|
+
* The table's TTL attribute: epoch seconds on every item of a row that
|
|
84
|
+
* expires, which DynamoDB deletes for free. No column is named like it.
|
|
85
|
+
*/
|
|
86
|
+
const DYNAMODB_TTL_ATTRIBUTE = "_ttl";
|
|
87
|
+
/** A column named like a key attribute is stored with a leading dot, which no column name has. */
|
|
88
|
+
const attribute = (column) => column === "pk" || column === "sk" ? `.${column}` : column;
|
|
89
|
+
const renamed = (record, rename) => Object.fromEntries(Object.entries(record).map(([name, value]) => [rename(name), value]));
|
|
90
|
+
const ttl = (expiresAt) => expiresAt === void 0 ? {} : { _ttl: Math.ceil(expiresAt / 1e3) };
|
|
91
|
+
const toItem = (key, row, expiresAt) => ({
|
|
92
|
+
...renamed({
|
|
93
|
+
...row,
|
|
94
|
+
...ttl(expiresAt)
|
|
95
|
+
}, attribute),
|
|
96
|
+
pk: key.pk,
|
|
97
|
+
sk: key.sk
|
|
98
|
+
});
|
|
99
|
+
const toRow = ({ pk: _pk, sk: _sk, _ttl, ...item }) => renamed(item, (name) => name.replace(/^\./, ""));
|
|
100
|
+
const idOf = ({ pk, sk }) => JSON.stringify([pk, sk]);
|
|
101
|
+
const nameOf = (error) => error.name;
|
|
102
|
+
const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
103
|
+
/**
|
|
104
|
+
* One transaction item. Its expressions bind every column name and value, so
|
|
105
|
+
* no column clashes with a reserved word; `pk` and `sk` are none.
|
|
106
|
+
*/
|
|
107
|
+
const toTransactItem = (tableName, op) => {
|
|
515
108
|
const names = {};
|
|
516
109
|
const values = {};
|
|
517
|
-
const
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
const valueName = model === "bundles" ? "bundle" : "patch";
|
|
522
|
-
names[`#${name}`] = name;
|
|
523
|
-
values[`:${valueName}Delta`] = change;
|
|
524
|
-
updates.push(`#${name} :${valueName}Delta`);
|
|
525
|
-
if (change < 0) {
|
|
526
|
-
values[`:${valueName}Removal`] = -change;
|
|
527
|
-
conditions.push(`#${name} >= :${valueName}Removal`);
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
return { Update: {
|
|
531
|
-
TableName: store.tableName,
|
|
532
|
-
Key: {
|
|
533
|
-
pk: "_hot-updater",
|
|
534
|
-
sk: "limits.metadata"
|
|
535
|
-
},
|
|
536
|
-
UpdateExpression: `ADD ${updates.join(", ")}`,
|
|
537
|
-
...conditions.length > 0 ? { ConditionExpression: conditions.join(" AND ") } : {},
|
|
538
|
-
ExpressionAttributeNames: names,
|
|
539
|
-
ExpressionAttributeValues: values
|
|
540
|
-
} };
|
|
541
|
-
};
|
|
542
|
-
const updateBundleRelation = (store, bundleId, delta, ownedPatchDelta = 0) => {
|
|
543
|
-
const names = {
|
|
544
|
-
"#pk": "pk",
|
|
545
|
-
"#version": "version"
|
|
110
|
+
const binder = (map, prefix) => (entry) => {
|
|
111
|
+
const token = `${prefix}${Object.keys(map).length}`;
|
|
112
|
+
map[token] = entry;
|
|
113
|
+
return token;
|
|
546
114
|
};
|
|
547
|
-
const
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
condition += " AND #relationCount >= :relationRemoval";
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
if (ownedPatchDelta !== 0) {
|
|
561
|
-
names["#ownedPatchCount"] = "owned_patch_count";
|
|
562
|
-
values[":ownedPatchDelta"] = ownedPatchDelta;
|
|
563
|
-
additions.push("#ownedPatchCount :ownedPatchDelta");
|
|
564
|
-
if (ownedPatchDelta < 0) {
|
|
565
|
-
values[":ownedPatchRemoval"] = -ownedPatchDelta;
|
|
566
|
-
condition += " AND #ownedPatchCount >= :ownedPatchRemoval";
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
if (additions.length > 0) update += ` ADD ${additions.join(", ")}`;
|
|
570
|
-
return { Update: {
|
|
571
|
-
TableName: store.tableName,
|
|
572
|
-
Key: itemKey("bundles", bundleId),
|
|
573
|
-
ConditionExpression: condition,
|
|
574
|
-
ExpressionAttributeNames: names,
|
|
575
|
-
ExpressionAttributeValues: values,
|
|
576
|
-
UpdateExpression: update
|
|
577
|
-
} };
|
|
578
|
-
};
|
|
579
|
-
const commitDynamoDBTransaction = async (store, actions) => {
|
|
580
|
-
if (actions.length > 100) throw new DynamoDBTransactionLimitError(actions.length);
|
|
581
|
-
await store.client.send(new _aws_sdk_lib_dynamodb.TransactWriteCommand({ TransactItems: [...actions] }));
|
|
582
|
-
};
|
|
583
|
-
const DYNAMODB_BATCH_GET_ATTEMPTS = 5;
|
|
584
|
-
const DYNAMODB_BATCH_GET_BASE_DELAY_MS = 25;
|
|
585
|
-
var DynamoDBBatchGetExhaustedError = class extends Error {
|
|
586
|
-
unprocessedKeyCount;
|
|
587
|
-
name = "DynamoDBBatchGetExhaustedError";
|
|
588
|
-
constructor(unprocessedKeyCount) {
|
|
589
|
-
super(`DynamoDB did not process ${unprocessedKeyCount} batch-get keys after ${DYNAMODB_BATCH_GET_ATTEMPTS} attempts`);
|
|
590
|
-
this.unprocessedKeyCount = unprocessedKeyCount;
|
|
591
|
-
}
|
|
592
|
-
};
|
|
593
|
-
const waitBeforeRetry = (attempt) => new Promise((resolve) => setTimeout(resolve, DYNAMODB_BATCH_GET_BASE_DELAY_MS * 2 ** attempt));
|
|
594
|
-
const batchGetDynamoDBItems = async (store, keys) => {
|
|
595
|
-
const items = [];
|
|
596
|
-
for (let offset = 0; offset < keys.length; offset += 100) {
|
|
597
|
-
let pending = keys.slice(offset, offset + 100);
|
|
598
|
-
for (let attempt = 0; attempt < DYNAMODB_BATCH_GET_ATTEMPTS && pending.length > 0; attempt++) {
|
|
599
|
-
const { Responses, UnprocessedKeys } = await store.client.send(new _aws_sdk_lib_dynamodb.BatchGetCommand({ RequestItems: { [store.tableName]: {
|
|
600
|
-
ConsistentRead: true,
|
|
601
|
-
Keys: pending
|
|
602
|
-
} } }));
|
|
603
|
-
items.push(...Responses?.[store.tableName] ?? []);
|
|
604
|
-
pending = UnprocessedKeys?.[store.tableName]?.Keys ?? [];
|
|
605
|
-
if (pending.length > 0 && attempt + 1 < DYNAMODB_BATCH_GET_ATTEMPTS) await waitBeforeRetry(attempt);
|
|
606
|
-
}
|
|
607
|
-
if (pending.length > 0) throw new DynamoDBBatchGetExhaustedError(pending.length);
|
|
608
|
-
}
|
|
609
|
-
return items;
|
|
610
|
-
};
|
|
611
|
-
const parseKeyReadItem = (item) => parseDynamoDBItem(boundedDynamoDBMetadataItem(item));
|
|
612
|
-
const loadBundleItemsById = async (store, ids) => {
|
|
613
|
-
const uniqueIds = [...new Set(ids)];
|
|
614
|
-
if (uniqueIds.length === 0) return [];
|
|
615
|
-
return (await batchGetDynamoDBItems(store, uniqueIds.map((id) => itemKey("bundles", id)))).map(parseKeyReadItem).filter((item) => item.pk === "bundles");
|
|
616
|
-
};
|
|
617
|
-
const loadPatchItemsById = async (store, ids) => {
|
|
618
|
-
const uniqueIds = [...new Set(ids)];
|
|
619
|
-
if (uniqueIds.length === 0) return [];
|
|
620
|
-
return (await batchGetDynamoDBItems(store, uniqueIds.map((id) => itemKey("bundle_patches", id)))).map(parseKeyReadItem).filter((item) => item.pk === "bundle_patches");
|
|
621
|
-
};
|
|
622
|
-
const loadMetadataCount = async (store, model) => {
|
|
623
|
-
if (model !== "bundles" && model !== "bundle_patches") return void 0;
|
|
624
|
-
const field = model === "bundles" ? "bundles" : "patches";
|
|
625
|
-
const { Item } = await store.client.send(new _aws_sdk_lib_dynamodb.GetCommand({
|
|
626
|
-
TableName: store.tableName,
|
|
627
|
-
Key: {
|
|
628
|
-
pk: "_hot-updater",
|
|
629
|
-
sk: "limits.metadata"
|
|
630
|
-
},
|
|
631
|
-
ConsistentRead: true,
|
|
632
|
-
ProjectionExpression: "#count",
|
|
633
|
-
ExpressionAttributeNames: { "#count": field }
|
|
634
|
-
}));
|
|
635
|
-
const count = Item?.[field];
|
|
636
|
-
return typeof count === "number" && Number.isSafeInteger(count) && count >= 0 ? count : void 0;
|
|
637
|
-
};
|
|
638
|
-
var DynamoDBPatchIndexConsistencyError = class extends Error {
|
|
639
|
-
bundleId;
|
|
640
|
-
expectedCount;
|
|
641
|
-
name = "DynamoDBPatchIndexConsistencyError";
|
|
642
|
-
constructor(bundleId, expectedCount) {
|
|
643
|
-
super(`DynamoDB patch index did not converge to ${expectedCount} rows for bundle "${bundleId}"`);
|
|
644
|
-
this.bundleId = bundleId;
|
|
645
|
-
this.expectedCount = expectedCount;
|
|
646
|
-
}
|
|
647
|
-
};
|
|
648
|
-
const waitForIndex = (attempt) => new Promise((resolve) => setTimeout(resolve, 20 * (attempt + 1)));
|
|
649
|
-
const queryCompleteOwnerPatches = async (store, indexName, bundleId) => {
|
|
650
|
-
const owner = await loadBundleItem(store, bundleId);
|
|
651
|
-
if (!owner || owner.owned_patch_count === 0) return [];
|
|
652
|
-
for (let attempt = 0; attempt < 3; attempt++) {
|
|
653
|
-
const patchIds = await queryOwnerPatchIds(store, indexName, bundleId);
|
|
654
|
-
const patchIdSet = new Set(patchIds);
|
|
655
|
-
const patches = (await loadPatchItemsById(store, patchIds)).filter(({ row }) => row.bundle_id === bundleId);
|
|
656
|
-
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));
|
|
657
|
-
if (attempt < 2) await waitForIndex(attempt);
|
|
658
|
-
}
|
|
659
|
-
throw new DynamoDBPatchIndexConsistencyError(bundleId, owner.owned_patch_count);
|
|
660
|
-
};
|
|
661
|
-
const queryCompleteOwnersPatches = async (store, indexName, bundleIds) => {
|
|
662
|
-
const owners = (await loadBundleItemsById(store, bundleIds)).filter(({ owned_patch_count }) => owned_patch_count > 0);
|
|
663
|
-
const firstOwner = owners[0];
|
|
664
|
-
if (!firstOwner) return [];
|
|
665
|
-
let failedOwner = firstOwner;
|
|
666
|
-
for (let attempt = 0; attempt < 3; attempt++) {
|
|
667
|
-
const candidateIdsByOwner = await Promise.all(owners.map(({ sk }) => queryOwnerPatchIds(store, indexName, sk)));
|
|
668
|
-
const candidateIds = candidateIdsByOwner.flat();
|
|
669
|
-
const patches = await loadPatchItemsById(store, candidateIds);
|
|
670
|
-
const patchesById = new Map(patches.map((patch) => [patch.sk, patch]));
|
|
671
|
-
const mismatchedOwner = owners.find((owner, ownerIndex) => {
|
|
672
|
-
const ids = candidateIdsByOwner[ownerIndex] ?? [];
|
|
673
|
-
const uniqueIds = new Set(ids);
|
|
674
|
-
return ids.length !== owner.owned_patch_count || uniqueIds.size !== owner.owned_patch_count || ids.some((id) => patchesById.get(id)?.row.bundle_id !== owner.sk);
|
|
675
|
-
});
|
|
676
|
-
if (!mismatchedOwner) return patches.map(({ row }) => row);
|
|
677
|
-
failedOwner = mismatchedOwner;
|
|
678
|
-
if (attempt < 2) await waitForIndex(attempt);
|
|
679
|
-
}
|
|
680
|
-
throw new DynamoDBPatchIndexConsistencyError(failedOwner.sk, failedOwner.owned_patch_count);
|
|
681
|
-
};
|
|
682
|
-
const createDynamoDBBundlePatchTable = (store, indexName) => ({ findByBundleIds: (bundleIds) => bundleIds.length === 0 ? Promise.resolve([]) : queryCompleteOwnersPatches(store, indexName, bundleIds) });
|
|
683
|
-
const compare = (left, right) => {
|
|
684
|
-
if (typeof left === "number" && typeof right === "number") return left - right;
|
|
685
|
-
if (left == null) return right == null ? 0 : -1;
|
|
686
|
-
if (right == null) return 1;
|
|
687
|
-
return JSON.stringify(left).localeCompare(JSON.stringify(right));
|
|
688
|
-
};
|
|
689
|
-
const compareString = (actual, expected, mode, predicate) => {
|
|
690
|
-
if (typeof actual !== "string") return false;
|
|
691
|
-
return mode === "insensitive" ? predicate(actual.toLocaleLowerCase(), expected.toLocaleLowerCase()) : predicate(actual, expected);
|
|
692
|
-
};
|
|
693
|
-
const matchesCondition = (row, condition) => {
|
|
694
|
-
const actual = Reflect.get(row, condition.field);
|
|
695
|
-
const expected = Reflect.get(condition, "value");
|
|
696
|
-
switch (Reflect.get(condition, "operator") ?? "eq") {
|
|
697
|
-
case "eq": return typeof expected === "string" ? compareString(actual, expected, Reflect.get(condition, "mode"), (value, query) => value === query) : actual === expected;
|
|
698
|
-
case "ne":
|
|
699
|
-
if (actual == null) return false;
|
|
700
|
-
return typeof expected === "string" ? !compareString(actual, expected, Reflect.get(condition, "mode"), (value, query) => value === query) : actual !== expected;
|
|
701
|
-
case "gt": return actual != null && compare(actual, expected) > 0;
|
|
702
|
-
case "gte": return actual != null && compare(actual, expected) >= 0;
|
|
703
|
-
case "lt": return actual != null && compare(actual, expected) < 0;
|
|
704
|
-
case "lte": return actual != null && compare(actual, expected) <= 0;
|
|
705
|
-
case "in": return Array.isArray(expected) && expected.some((candidate) => candidate === actual);
|
|
706
|
-
case "not_in": return Array.isArray(expected) && (expected.length === 0 || actual != null && expected.every((candidate) => candidate !== actual));
|
|
707
|
-
case "contains": return typeof expected === "string" ? compareString(actual, expected, Reflect.get(condition, "mode"), (value, query) => value.includes(query)) : false;
|
|
708
|
-
case "starts_with": return typeof expected === "string" ? compareString(actual, expected, Reflect.get(condition, "mode"), (value, query) => value.startsWith(query)) : false;
|
|
709
|
-
case "ends_with": return typeof expected === "string" ? compareString(actual, expected, Reflect.get(condition, "mode"), (value, query) => value.endsWith(query)) : false;
|
|
710
|
-
default: return false;
|
|
711
|
-
}
|
|
712
|
-
};
|
|
713
|
-
const matchesDynamoDBWhere = (row, where) => {
|
|
714
|
-
if (!where || where.length === 0) return true;
|
|
715
|
-
let result = matchesCondition(row, where[0]);
|
|
716
|
-
for (const condition of where.slice(1)) {
|
|
717
|
-
const current = matchesCondition(row, condition);
|
|
718
|
-
result = condition.connector === "OR" ? result || current : result && current;
|
|
719
|
-
}
|
|
720
|
-
return result;
|
|
721
|
-
};
|
|
722
|
-
const compareRows = (left, right, orderBy) => {
|
|
723
|
-
for (const clause of orderBy) {
|
|
724
|
-
const leftValue = Reflect.get(left, clause.field);
|
|
725
|
-
const rightValue = Reflect.get(right, clause.field);
|
|
726
|
-
if (leftValue == null || rightValue == null) {
|
|
727
|
-
if (leftValue == null && rightValue == null) continue;
|
|
728
|
-
const nulls = clause.nulls ?? (clause.direction === "asc" ? "last" : "first");
|
|
729
|
-
const order = leftValue == null ? -1 : 1;
|
|
730
|
-
return nulls === "first" ? order : -order;
|
|
731
|
-
}
|
|
732
|
-
const order = compare(leftValue, rightValue);
|
|
733
|
-
if (order !== 0) return clause.direction === "asc" ? order : -order;
|
|
734
|
-
}
|
|
735
|
-
return 0;
|
|
736
|
-
};
|
|
737
|
-
const distinctKey = (row, distinctOn) => JSON.stringify(distinctOn.fields.map((field) => Reflect.get(row, field)));
|
|
738
|
-
const queryDynamoDBRows = (rows, input) => {
|
|
739
|
-
const filtered = rows.filter((row) => matchesDynamoDBWhere(row, input.where));
|
|
740
|
-
const orderBy = input.orderBy;
|
|
741
|
-
const ordered = orderBy ? filtered.toSorted((left, right) => compareRows(left, right, orderBy)) : filtered;
|
|
742
|
-
const distinctOn = input.distinctOn;
|
|
743
|
-
const distinct = distinctOn ? (() => {
|
|
744
|
-
const seen = /* @__PURE__ */ new Set();
|
|
745
|
-
return ordered.filter((row) => {
|
|
746
|
-
const key = distinctKey(row, distinctOn);
|
|
747
|
-
if (seen.has(key)) return false;
|
|
748
|
-
seen.add(key);
|
|
749
|
-
return true;
|
|
750
|
-
});
|
|
751
|
-
})() : ordered;
|
|
752
|
-
const offset = input.offset ?? 0;
|
|
753
|
-
return distinct.slice(offset, offset + (input.limit ?? 100));
|
|
754
|
-
};
|
|
755
|
-
const countDistinctDynamoDBRows = (rows, fields) => {
|
|
756
|
-
if (fields === void 0) return rows.length;
|
|
757
|
-
return new Set(rows.map((row) => JSON.stringify(fields.map((field) => Reflect.get(row, field))))).size;
|
|
758
|
-
};
|
|
759
|
-
const createDynamoDBBundle = async (store, row) => {
|
|
760
|
-
const counter = metadataUpdate(store, { bundles: 1 });
|
|
761
|
-
if (!counter) return;
|
|
762
|
-
await commitDynamoDBTransaction(store, [counter, { Put: {
|
|
763
|
-
TableName: store.tableName,
|
|
764
|
-
Item: boundedDynamoDBMetadataItem(toDynamoDBBundleItem(row)),
|
|
765
|
-
ConditionExpression: "attribute_not_exists(#pk)",
|
|
766
|
-
ExpressionAttributeNames: { "#pk": "pk" }
|
|
767
|
-
} }]);
|
|
768
|
-
};
|
|
769
|
-
const createDynamoDBPatch = async (store, row) => {
|
|
770
|
-
const bundleIds = [.../* @__PURE__ */ new Set([row.bundle_id, row.base_bundle_id])];
|
|
771
|
-
const counter = metadataUpdate(store, { bundle_patches: 1 });
|
|
772
|
-
if (!counter) return;
|
|
773
|
-
await commitDynamoDBTransaction(store, [
|
|
774
|
-
counter,
|
|
775
|
-
...bundleIds.map((bundleId) => updateBundleRelation(store, bundleId, 1, bundleId === row.bundle_id ? 1 : 0)),
|
|
776
|
-
{ Put: {
|
|
777
|
-
TableName: store.tableName,
|
|
778
|
-
Item: boundedDynamoDBMetadataItem(toDynamoDBPatchItem(row)),
|
|
779
|
-
ConditionExpression: "attribute_not_exists(#pk)",
|
|
780
|
-
ExpressionAttributeNames: { "#pk": "pk" }
|
|
781
|
-
} }
|
|
782
|
-
]);
|
|
783
|
-
};
|
|
784
|
-
const replaceDynamoDBBundle = async (store, current, row) => {
|
|
785
|
-
await store.client.send(new _aws_sdk_lib_dynamodb.PutCommand({
|
|
786
|
-
TableName: store.tableName,
|
|
787
|
-
Item: boundedDynamoDBMetadataItem(toDynamoDBBundleItem(row, current.version + 1, current.relation_count, current.owned_patch_count)),
|
|
788
|
-
ConditionExpression: "#version = :currentVersion",
|
|
789
|
-
ExpressionAttributeNames: { "#version": "version" },
|
|
790
|
-
ExpressionAttributeValues: { ":currentVersion": current.version }
|
|
791
|
-
}));
|
|
792
|
-
};
|
|
793
|
-
const deleteAction = (store, item) => ({ Delete: {
|
|
794
|
-
TableName: store.tableName,
|
|
795
|
-
Key: {
|
|
796
|
-
pk: item.pk,
|
|
797
|
-
sk: item.sk
|
|
798
|
-
},
|
|
799
|
-
ConditionExpression: "#version = :currentVersion",
|
|
800
|
-
ExpressionAttributeNames: { "#version": "version" },
|
|
801
|
-
ExpressionAttributeValues: { ":currentVersion": item.version }
|
|
802
|
-
} });
|
|
803
|
-
const deleteDynamoDBPatch = async (store, item) => {
|
|
804
|
-
const counter = metadataUpdate(store, { bundle_patches: -1 });
|
|
805
|
-
if (!counter) return;
|
|
806
|
-
const bundleIds = [.../* @__PURE__ */ new Set([item.row.bundle_id, item.row.base_bundle_id])];
|
|
807
|
-
await commitDynamoDBTransaction(store, [
|
|
808
|
-
counter,
|
|
809
|
-
...bundleIds.map((id) => updateBundleRelation(store, id, -1, id === item.row.bundle_id ? -1 : 0)),
|
|
810
|
-
deleteAction(store, item)
|
|
811
|
-
]);
|
|
812
|
-
};
|
|
813
|
-
const deleteBundle = async (store, bundle, patches) => {
|
|
814
|
-
const counter = metadataUpdate(store, {
|
|
815
|
-
bundles: -1,
|
|
816
|
-
bundle_patches: -patches.length
|
|
115
|
+
const name = binder(names, "#n");
|
|
116
|
+
const value = binder(values, ":v");
|
|
117
|
+
const { condition } = op;
|
|
118
|
+
const guard = condition === void 0 ? void 0 : "v" in condition ? `${name("_v")} = ${value(condition.v)}` : `${condition.exists ? "attribute_exists" : "attribute_not_exists"}(pk)`;
|
|
119
|
+
const item = (fields) => ({
|
|
120
|
+
TableName: tableName,
|
|
121
|
+
ConditionExpression: guard,
|
|
122
|
+
...fields,
|
|
123
|
+
ExpressionAttributeNames: Object.keys(names).length ? names : void 0,
|
|
124
|
+
ExpressionAttributeValues: Object.keys(values).length ? values : void 0
|
|
817
125
|
});
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
if (id === bundle.sk) continue;
|
|
822
|
-
const current = otherEndpointCounts.get(id) ?? {
|
|
823
|
-
owned: 0,
|
|
824
|
-
relations: 0
|
|
825
|
-
};
|
|
826
|
-
otherEndpointCounts.set(id, {
|
|
827
|
-
owned: current.owned + (id === row.bundle_id ? 1 : 0),
|
|
828
|
-
relations: current.relations + 1
|
|
829
|
-
});
|
|
830
|
-
}
|
|
831
|
-
await commitDynamoDBTransaction(store, [
|
|
832
|
-
counter,
|
|
833
|
-
...[...otherEndpointCounts].map(([id, count]) => updateBundleRelation(store, id, -count.relations, -count.owned)),
|
|
834
|
-
...patches.map((patch) => deleteAction(store, patch)),
|
|
835
|
-
deleteAction(store, bundle)
|
|
836
|
-
]);
|
|
837
|
-
};
|
|
838
|
-
const deleteDynamoDBBundles = async (store, bundles, allPatches) => {
|
|
839
|
-
let patches = [...allPatches];
|
|
840
|
-
const versionIncrements = /* @__PURE__ */ new Map();
|
|
841
|
-
for (const originalBundle of bundles) {
|
|
842
|
-
const bundle = {
|
|
843
|
-
...originalBundle,
|
|
844
|
-
version: originalBundle.version + (versionIncrements.get(originalBundle.sk) ?? 0)
|
|
845
|
-
};
|
|
846
|
-
const related = patches.filter(({ row }) => row.bundle_id === bundle.sk || row.base_bundle_id === bundle.sk);
|
|
847
|
-
await deleteBundle(store, bundle, related);
|
|
848
|
-
const updatedBundleIds = new Set(related.flatMap(({ row }) => [row.bundle_id, row.base_bundle_id]));
|
|
849
|
-
updatedBundleIds.delete(bundle.sk);
|
|
850
|
-
for (const id of updatedBundleIds) versionIncrements.set(id, (versionIncrements.get(id) ?? 0) + 1);
|
|
851
|
-
const removedPatchIds = new Set(related.map(({ sk }) => sk));
|
|
852
|
-
patches = patches.filter(({ sk }) => !removedPatchIds.has(sk));
|
|
853
|
-
}
|
|
854
|
-
};
|
|
855
|
-
var DynamoDBUnsupportedModelError = class extends Error {
|
|
856
|
-
name = "DynamoDBUnsupportedModelError";
|
|
857
|
-
constructor() {
|
|
858
|
-
super("DynamoDB received an unsupported database model");
|
|
859
|
-
}
|
|
860
|
-
};
|
|
861
|
-
const distinctFields = (fields) => fields;
|
|
862
|
-
const exactDynamoDBField = (where, fieldName) => {
|
|
863
|
-
const conditions = (where ?? []).filter((condition) => Reflect.get(condition, "field") === fieldName);
|
|
864
|
-
if (conditions.length !== 1) return void 0;
|
|
865
|
-
const condition = conditions[0];
|
|
866
|
-
if ((Reflect.get(condition, "operator") ?? "eq") !== "eq" || Reflect.get(condition, "mode") === "insensitive") return;
|
|
867
|
-
const value = Reflect.get(condition, "value");
|
|
868
|
-
return typeof value === "string" ? value : void 0;
|
|
869
|
-
};
|
|
870
|
-
const createDynamoDBCrud = (store, updateIndexName) => ({
|
|
871
|
-
recordInsights: (input) => recordDynamoDBInsightsEvent(store, input),
|
|
872
|
-
async create(input) {
|
|
873
|
-
switch (input.model) {
|
|
874
|
-
case "bundles":
|
|
875
|
-
await createDynamoDBBundle(store, input.data);
|
|
876
|
-
return input.data;
|
|
877
|
-
case "bundle_patches":
|
|
878
|
-
await createDynamoDBPatch(store, input.data);
|
|
879
|
-
return input.data;
|
|
880
|
-
case "releases": {
|
|
881
|
-
const channel = await loadChannelItem(store, input.data.channel_id);
|
|
882
|
-
if (channel === void 0) throw new DynamoDBStoredItemError();
|
|
883
|
-
await commitDynamoDBTransaction(store, [
|
|
884
|
-
updateChannelReferenceCount(store, channel, 1),
|
|
885
|
-
{ Put: {
|
|
886
|
-
TableName: store.tableName,
|
|
887
|
-
Item: boundedDynamoDBMetadataItem(toDynamoDBReleaseItem(input.data)),
|
|
888
|
-
ConditionExpression: "attribute_not_exists(#pk)",
|
|
889
|
-
ExpressionAttributeNames: { "#pk": "pk" }
|
|
890
|
-
} },
|
|
891
|
-
{ Put: {
|
|
892
|
-
TableName: store.tableName,
|
|
893
|
-
Item: releaseLocatorItem(input.data),
|
|
894
|
-
ConditionExpression: "attribute_not_exists(#pk)",
|
|
895
|
-
ExpressionAttributeNames: { "#pk": "pk" }
|
|
896
|
-
} }
|
|
897
|
-
]);
|
|
898
|
-
return input.data;
|
|
899
|
-
}
|
|
900
|
-
case "release_catalogs":
|
|
901
|
-
await store.client.send(new _aws_sdk_lib_dynamodb.PutCommand({
|
|
902
|
-
TableName: store.tableName,
|
|
903
|
-
Item: boundedDynamoDBCatalogItem(toDynamoDBReleaseCatalogItem(input.data)),
|
|
904
|
-
ConditionExpression: "attribute_not_exists(#pk)",
|
|
905
|
-
ExpressionAttributeNames: { "#pk": "pk" }
|
|
906
|
-
}));
|
|
907
|
-
return input.data;
|
|
908
|
-
case "channels": return (await insertDynamoDBChannel(store, {
|
|
909
|
-
row: input.data,
|
|
910
|
-
onConflict: "returnExisting"
|
|
911
|
-
})).row;
|
|
912
|
-
}
|
|
913
|
-
throw new DynamoDBUnsupportedModelError();
|
|
914
|
-
},
|
|
915
|
-
async update(input) {
|
|
916
|
-
if (input.model === "releases") {
|
|
917
|
-
const id = exactDynamoDBId(input.where);
|
|
918
|
-
if (id === void 0) throw new DynamoDBUnsupportedModelError();
|
|
919
|
-
const current = await loadReleaseItem(store, id);
|
|
920
|
-
if (current === void 0) return null;
|
|
921
|
-
const updated = {
|
|
922
|
-
...current.row,
|
|
923
|
-
...input.update
|
|
924
|
-
};
|
|
925
|
-
const nextPartition = releaseScopePartition(updated.scope_key);
|
|
926
|
-
const moved = current.pk !== nextPartition;
|
|
927
|
-
await commitDynamoDBTransaction(store, [
|
|
928
|
-
...moved ? [{ Delete: {
|
|
929
|
-
TableName: store.tableName,
|
|
930
|
-
Key: {
|
|
931
|
-
pk: current.pk,
|
|
932
|
-
sk: current.sk
|
|
933
|
-
},
|
|
934
|
-
ConditionExpression: "#version = :version",
|
|
935
|
-
ExpressionAttributeNames: { "#version": "version" },
|
|
936
|
-
ExpressionAttributeValues: { ":version": current.version }
|
|
937
|
-
} }] : [],
|
|
938
|
-
{ Put: {
|
|
939
|
-
TableName: store.tableName,
|
|
940
|
-
Item: boundedDynamoDBMetadataItem(toDynamoDBReleaseItem(updated, current.version + 1)),
|
|
941
|
-
ConditionExpression: moved ? "attribute_not_exists(#pk)" : "#version = :version",
|
|
942
|
-
ExpressionAttributeNames: moved ? { "#pk": "pk" } : { "#version": "version" },
|
|
943
|
-
...moved ? {} : { ExpressionAttributeValues: { ":version": current.version } }
|
|
944
|
-
} },
|
|
945
|
-
{ Put: {
|
|
946
|
-
TableName: store.tableName,
|
|
947
|
-
Item: releaseLocatorItem(updated),
|
|
948
|
-
ConditionExpression: "#scopeKey = :scopeKey",
|
|
949
|
-
ExpressionAttributeNames: { "#scopeKey": "scope_key" },
|
|
950
|
-
ExpressionAttributeValues: { ":scopeKey": current.row.scope_key }
|
|
951
|
-
} }
|
|
952
|
-
]);
|
|
953
|
-
return updated;
|
|
954
|
-
}
|
|
955
|
-
if (input.model === "release_catalogs") {
|
|
956
|
-
const scopeKey = exactDynamoDBField(input.where, "scope_key");
|
|
957
|
-
if (scopeKey === void 0) throw new DynamoDBUnsupportedModelError();
|
|
958
|
-
const current = await loadReleaseCatalogItem(store, scopeKey);
|
|
959
|
-
if (current === void 0) return null;
|
|
960
|
-
const updated = {
|
|
961
|
-
...current.row,
|
|
962
|
-
...input.update
|
|
963
|
-
};
|
|
964
|
-
await store.client.send(new _aws_sdk_lib_dynamodb.PutCommand({
|
|
965
|
-
TableName: store.tableName,
|
|
966
|
-
Item: boundedDynamoDBCatalogItem(toDynamoDBReleaseCatalogItem(updated, current.version + 1)),
|
|
967
|
-
ConditionExpression: "#version = :version",
|
|
968
|
-
ExpressionAttributeNames: { "#version": "version" },
|
|
969
|
-
ExpressionAttributeValues: { ":version": current.version }
|
|
970
|
-
}));
|
|
971
|
-
return updated;
|
|
972
|
-
}
|
|
973
|
-
if (input.model !== "bundles") throw new DynamoDBUnsupportedModelError();
|
|
974
|
-
const id = exactDynamoDBId(input.where);
|
|
975
|
-
const current = id === void 0 ? (await loadBundleItems(store)).find(({ row }) => matchesDynamoDBWhere(row, input.where)) : await loadBundleItem(store, id);
|
|
976
|
-
if (!current) return null;
|
|
977
|
-
const updated = {
|
|
978
|
-
...current.row,
|
|
979
|
-
...input.update
|
|
980
|
-
};
|
|
981
|
-
await replaceDynamoDBBundle(store, current, updated);
|
|
982
|
-
return updated;
|
|
983
|
-
},
|
|
984
|
-
async delete(input) {
|
|
985
|
-
if (input.model === "channels") {
|
|
986
|
-
const id = exactDynamoDBId(input.where);
|
|
987
|
-
if (id !== void 0) await deleteDynamoDBChannel(store, { id });
|
|
988
|
-
return;
|
|
989
|
-
}
|
|
990
|
-
if (input.model === "bundle_patches") {
|
|
991
|
-
const items = (await loadPatchItems(store)).filter(({ row }) => matchesDynamoDBWhere(row, input.where));
|
|
992
|
-
for (const item of items) await deleteDynamoDBPatch(store, item);
|
|
993
|
-
return;
|
|
994
|
-
}
|
|
995
|
-
if (input.model === "releases") {
|
|
996
|
-
const items = (await loadReleaseItems(store)).filter(({ row }) => matchesDynamoDBWhere(row, input.where));
|
|
997
|
-
for (const item of items) {
|
|
998
|
-
const channel = await loadChannelItem(store, item.row.channel_id);
|
|
999
|
-
if (channel === void 0) throw new DynamoDBStoredItemError();
|
|
1000
|
-
await commitDynamoDBTransaction(store, [
|
|
1001
|
-
updateChannelReferenceCount(store, channel, -1),
|
|
1002
|
-
deleteAction(store, item),
|
|
1003
|
-
{ Delete: {
|
|
1004
|
-
TableName: store.tableName,
|
|
1005
|
-
Key: {
|
|
1006
|
-
pk: DYNAMODB_RELEASE_ID_PARTITION,
|
|
1007
|
-
sk: item.sk
|
|
1008
|
-
},
|
|
1009
|
-
ConditionExpression: "#scopeKey = :scopeKey",
|
|
1010
|
-
ExpressionAttributeNames: { "#scopeKey": "scope_key" },
|
|
1011
|
-
ExpressionAttributeValues: { ":scopeKey": item.row.scope_key }
|
|
1012
|
-
} }
|
|
1013
|
-
]);
|
|
1014
|
-
}
|
|
1015
|
-
return;
|
|
1016
|
-
}
|
|
1017
|
-
if (input.model !== "bundles") throw new DynamoDBUnsupportedModelError();
|
|
1018
|
-
const bundleItems = (await loadBundleItems(store)).filter(({ row }) => matchesDynamoDBWhere(row, input.where));
|
|
1019
|
-
if (bundleItems.length === 0) return;
|
|
1020
|
-
await deleteDynamoDBBundles(store, bundleItems, await loadPatchItems(store));
|
|
1021
|
-
},
|
|
1022
|
-
async count(input) {
|
|
1023
|
-
if ((input.where === void 0 || input.where.length === 0) && input.distinct === void 0) {
|
|
1024
|
-
const count = await loadMetadataCount(store, input.model);
|
|
1025
|
-
if (count !== void 0) return count;
|
|
1026
|
-
}
|
|
1027
|
-
switch (input.model) {
|
|
1028
|
-
case "bundles": {
|
|
1029
|
-
const rows = (await loadBundleItems(store)).map(({ row }) => row).filter((row) => matchesDynamoDBWhere(row, input.where));
|
|
1030
|
-
return countDistinctDynamoDBRows(rows, distinctFields(input.distinct));
|
|
1031
|
-
}
|
|
1032
|
-
case "bundle_patches": {
|
|
1033
|
-
const ownerId = exactDynamoDBPatchOwner(input.where);
|
|
1034
|
-
const rows = (ownerId ? await queryCompleteOwnerPatches(store, updateIndexName, ownerId) : (await loadPatchItems(store)).map(({ row }) => row)).filter((row) => matchesDynamoDBWhere(row, input.where));
|
|
1035
|
-
return countDistinctDynamoDBRows(rows, distinctFields(input.distinct));
|
|
1036
|
-
}
|
|
1037
|
-
case "releases": {
|
|
1038
|
-
const scopeKey = exactDynamoDBField(input.where, "scope_key");
|
|
1039
|
-
const rows = (scopeKey === void 0 ? await loadReleaseItems(store) : await loadReleaseItemsByScope(store, scopeKey)).map(({ row }) => row).filter((row) => matchesDynamoDBWhere(row, input.where));
|
|
1040
|
-
return countDistinctDynamoDBRows(rows, distinctFields(input.distinct));
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
throw new DynamoDBUnsupportedModelError();
|
|
1044
|
-
},
|
|
1045
|
-
async findOne(input) {
|
|
1046
|
-
const id = exactDynamoDBId(input.where);
|
|
1047
|
-
switch (input.model) {
|
|
1048
|
-
case "bundles":
|
|
1049
|
-
if (id !== void 0) return (await loadBundleItem(store, id))?.row ?? null;
|
|
1050
|
-
return (await loadBundleItems(store)).map(({ row }) => row).find((row) => matchesDynamoDBWhere(row, input.where)) ?? null;
|
|
1051
|
-
case "bundle_patches":
|
|
1052
|
-
if (id !== void 0) return (await loadPatchItem(store, id))?.row ?? null;
|
|
1053
|
-
{
|
|
1054
|
-
const ownerId = exactDynamoDBPatchOwner(input.where);
|
|
1055
|
-
if (ownerId === void 0) break;
|
|
1056
|
-
return (await queryCompleteOwnerPatches(store, updateIndexName, ownerId)).find((row) => matchesDynamoDBWhere(row, input.where)) ?? null;
|
|
1057
|
-
}
|
|
1058
|
-
case "channels":
|
|
1059
|
-
if (id !== void 0) return (await loadChannelItem(store, id))?.row ?? null;
|
|
1060
|
-
return (await loadChannelItems(store)).map(({ row }) => row).find((row) => matchesDynamoDBWhere(row, input.where)) ?? null;
|
|
1061
|
-
case "releases":
|
|
1062
|
-
if (id !== void 0) return (await loadReleaseItem(store, id))?.row ?? null;
|
|
1063
|
-
{
|
|
1064
|
-
const scopeKey = exactDynamoDBField(input.where, "scope_key");
|
|
1065
|
-
return (scopeKey === void 0 ? await loadReleaseItems(store) : await loadReleaseItemsByScope(store, scopeKey)).map(({ row }) => row).find((row) => matchesDynamoDBWhere(row, input.where)) ?? null;
|
|
1066
|
-
}
|
|
1067
|
-
case "release_catalogs": {
|
|
1068
|
-
const scopeKey = exactDynamoDBField(input.where, "scope_key");
|
|
1069
|
-
if (scopeKey !== void 0) return (await loadReleaseCatalogItem(store, scopeKey))?.row ?? null;
|
|
1070
|
-
return (await loadReleaseCatalogItems(store)).map(({ row }) => row).find((row) => matchesDynamoDBWhere(row, input.where)) ?? null;
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
if (input.model === "bundle_patches") return (await loadPatchItems(store)).map(({ row }) => row).find((row) => matchesDynamoDBWhere(row, input.where)) ?? null;
|
|
1074
|
-
throw new DynamoDBUnsupportedModelError();
|
|
1075
|
-
},
|
|
1076
|
-
async findMany(input) {
|
|
1077
|
-
if (input.limit === 0) return [];
|
|
1078
|
-
switch (input.model) {
|
|
1079
|
-
case "bundles": {
|
|
1080
|
-
const ids = exactDynamoDBBundleIds(input.where);
|
|
1081
|
-
if (ids !== void 0) return queryDynamoDBRows((await loadBundleItemsById(store, ids)).map(({ row }) => row), input);
|
|
1082
|
-
const orderBy = input.orderBy;
|
|
1083
|
-
const direction = orderBy?.[0]?.direction;
|
|
1084
|
-
if ((input.offset ?? 0) === 0 && input.distinctOn === void 0 && orderBy?.length === 1 && orderBy[0]?.field === "id" && (direction === "asc" || direction === "desc")) return (await queryBundleItemsPage(store, {
|
|
1085
|
-
direction,
|
|
1086
|
-
limit: input.limit ?? 100,
|
|
1087
|
-
matches: (row) => matchesDynamoDBWhere(row, input.where),
|
|
1088
|
-
start: dynamoDBBundlePageStart(input.where, direction)
|
|
1089
|
-
})).map(({ row }) => row);
|
|
1090
|
-
return queryDynamoDBRows((await loadBundleItems(store)).map(({ row }) => row), input);
|
|
1091
|
-
}
|
|
1092
|
-
case "bundle_patches": {
|
|
1093
|
-
const ownerIds = exactDynamoDBPatchOwners(input.where);
|
|
1094
|
-
if (ownerIds !== void 0) {
|
|
1095
|
-
const ownerId = ownerIds.length === 1 ? ownerIds[0] : void 0;
|
|
1096
|
-
return queryDynamoDBRows(ownerId !== void 0 ? await queryCompleteOwnerPatches(store, updateIndexName, ownerId) : await queryCompleteOwnersPatches(store, updateIndexName, ownerIds), input);
|
|
1097
|
-
}
|
|
1098
|
-
return queryDynamoDBRows((await loadPatchItems(store)).map(({ row }) => row), input);
|
|
1099
|
-
}
|
|
1100
|
-
case "channels": return queryDynamoDBRows((await loadChannelItems(store)).map(({ row }) => row), input);
|
|
1101
|
-
case "releases": {
|
|
1102
|
-
const scopeKey = exactDynamoDBField(input.where, "scope_key");
|
|
1103
|
-
return queryDynamoDBRows((scopeKey === void 0 ? await loadReleaseItems(store) : await loadReleaseItemsByScope(store, scopeKey)).map(({ row }) => row), input);
|
|
1104
|
-
}
|
|
1105
|
-
case "release_catalogs": return queryDynamoDBRows((await loadReleaseCatalogItems(store)).map(({ row }) => row), input);
|
|
1106
|
-
}
|
|
1107
|
-
throw new DynamoDBUnsupportedModelError();
|
|
1108
|
-
},
|
|
1109
|
-
insertChannel: (input) => insertDynamoDBChannel(store, input),
|
|
1110
|
-
deleteChannel: (input) => deleteDynamoDBChannel(store, input)
|
|
1111
|
-
});
|
|
1112
|
-
var DynamoDBDuplicatePatchError = class extends Error {
|
|
1113
|
-
patchId;
|
|
1114
|
-
name = "DynamoDBDuplicatePatchError";
|
|
1115
|
-
constructor(patchId) {
|
|
1116
|
-
super(`DynamoDB bundle mutation contains duplicate patch "${patchId}"`);
|
|
1117
|
-
this.patchId = patchId;
|
|
1118
|
-
}
|
|
1119
|
-
};
|
|
1120
|
-
const putNewBundle = (store, row, relationCount, ownedPatchCount = relationCount) => ({ Put: {
|
|
1121
|
-
TableName: store.tableName,
|
|
1122
|
-
Item: boundedDynamoDBMetadataItem(toDynamoDBBundleItem(row, 1, relationCount, ownedPatchCount)),
|
|
1123
|
-
ConditionExpression: "attribute_not_exists(#pk)",
|
|
1124
|
-
ExpressionAttributeNames: { "#pk": "pk" }
|
|
1125
|
-
} });
|
|
1126
|
-
const putUpdatedBundle = (store, current, row, relationCount, ownedPatchCount) => ({ Put: {
|
|
1127
|
-
TableName: store.tableName,
|
|
1128
|
-
Item: boundedDynamoDBMetadataItem(toDynamoDBBundleItem(row, current.version + 1, relationCount, ownedPatchCount)),
|
|
1129
|
-
ConditionExpression: "#version = :currentVersion",
|
|
1130
|
-
ExpressionAttributeNames: { "#version": "version" },
|
|
1131
|
-
ExpressionAttributeValues: { ":currentVersion": current.version }
|
|
1132
|
-
} });
|
|
1133
|
-
const putPatch = (store, row, current) => ({ Put: {
|
|
1134
|
-
TableName: store.tableName,
|
|
1135
|
-
Item: boundedDynamoDBMetadataItem(toDynamoDBPatchItem(row, (current?.version ?? 0) + 1)),
|
|
1136
|
-
ConditionExpression: current ? "#version = :currentVersion" : "attribute_not_exists(#pk)",
|
|
1137
|
-
ExpressionAttributeNames: current ? { "#version": "version" } : { "#pk": "pk" },
|
|
1138
|
-
...current ? { ExpressionAttributeValues: { ":currentVersion": current.version } } : {}
|
|
1139
|
-
} });
|
|
1140
|
-
const deletePatch = (store, item) => ({ Delete: {
|
|
1141
|
-
TableName: store.tableName,
|
|
1142
|
-
Key: itemKey(item.pk, item.sk),
|
|
1143
|
-
ConditionExpression: "#version = :currentVersion",
|
|
1144
|
-
ExpressionAttributeNames: { "#version": "version" },
|
|
1145
|
-
ExpressionAttributeValues: { ":currentVersion": item.version }
|
|
1146
|
-
} });
|
|
1147
|
-
var DynamoDBCommitStateError = class extends Error {
|
|
1148
|
-
name = "DynamoDBCommitStateError";
|
|
1149
|
-
constructor(message) {
|
|
1150
|
-
super(message);
|
|
1151
|
-
}
|
|
1152
|
-
};
|
|
1153
|
-
const rowsEqual = (left, right) => JSON.stringify(left) === JSON.stringify(right);
|
|
1154
|
-
const putChannel = (store, row, current, referenceCount) => ({ Put: {
|
|
1155
|
-
TableName: store.tableName,
|
|
1156
|
-
Item: boundedDynamoDBMetadataItem(toDynamoDBChannelItem(row, (current?.version ?? 0) + 1, referenceCount)),
|
|
1157
|
-
ConditionExpression: current ? "#version = :version" : "attribute_not_exists(#pk)",
|
|
1158
|
-
ExpressionAttributeNames: current ? { "#version": "version" } : { "#pk": "pk" },
|
|
1159
|
-
...current ? { ExpressionAttributeValues: { ":version": current.version } } : {}
|
|
1160
|
-
} });
|
|
1161
|
-
const deleteChannelActions = (store, current) => [{ Delete: {
|
|
1162
|
-
TableName: store.tableName,
|
|
1163
|
-
Key: itemKey(DYNAMODB_CHANNEL_PARTITION, current.sk),
|
|
1164
|
-
ConditionExpression: "#version = :version",
|
|
1165
|
-
ExpressionAttributeNames: { "#version": "version" },
|
|
1166
|
-
ExpressionAttributeValues: { ":version": current.version }
|
|
1167
|
-
} }, { Delete: {
|
|
1168
|
-
TableName: store.tableName,
|
|
1169
|
-
Key: {
|
|
1170
|
-
pk: DYNAMODB_CHANNEL_NAME_PARTITION,
|
|
1171
|
-
sk: current.row.name
|
|
1172
|
-
},
|
|
1173
|
-
ConditionExpression: "#channelId = :channelId",
|
|
1174
|
-
ExpressionAttributeNames: { "#channelId": "channel_id" },
|
|
1175
|
-
ExpressionAttributeValues: { ":channelId": current.sk }
|
|
1176
|
-
} }];
|
|
1177
|
-
const updateChannelReferenceCount = (store, current, delta) => ({ Update: {
|
|
1178
|
-
TableName: store.tableName,
|
|
1179
|
-
Key: itemKey(DYNAMODB_CHANNEL_PARTITION, current.sk),
|
|
1180
|
-
ConditionExpression: "#row.#name = :name" + (delta < 0 ? " AND #referenceCount >= :removal" : ""),
|
|
1181
|
-
UpdateExpression: "SET #version = #version + :one ADD #referenceCount :delta",
|
|
1182
|
-
ExpressionAttributeNames: {
|
|
1183
|
-
"#row": "row",
|
|
1184
|
-
"#name": "name",
|
|
1185
|
-
"#version": "version",
|
|
1186
|
-
"#referenceCount": "reference_count"
|
|
1187
|
-
},
|
|
1188
|
-
ExpressionAttributeValues: {
|
|
1189
|
-
":name": current.row.name,
|
|
1190
|
-
":one": 1,
|
|
1191
|
-
":delta": delta,
|
|
1192
|
-
...delta < 0 ? { ":removal": -delta } : {}
|
|
1193
|
-
}
|
|
1194
|
-
} });
|
|
1195
|
-
const loadApiKeyByHash = async (store, hash) => {
|
|
1196
|
-
const { Item } = await store.client.send(new _aws_sdk_lib_dynamodb.GetCommand({
|
|
1197
|
-
TableName: store.tableName,
|
|
1198
|
-
Key: {
|
|
1199
|
-
pk: DYNAMODB_API_KEY_HASH_PARTITION,
|
|
1200
|
-
sk: hash
|
|
1201
|
-
},
|
|
1202
|
-
ConsistentRead: true
|
|
1203
|
-
}));
|
|
1204
|
-
if (Item === void 0) return null;
|
|
1205
|
-
const id = Item.api_key_id;
|
|
1206
|
-
if (typeof id !== "string") throw new DynamoDBStoredItemError();
|
|
1207
|
-
return loadApiKey(store, id);
|
|
1208
|
-
};
|
|
1209
|
-
const compileAndCommitDynamoDBChanges = async (store, input) => {
|
|
1210
|
-
const [originalBundleItems, originalPatchItems, originalChannelItems, originalReleaseItems, originalReleaseCatalogItems] = await Promise.all([
|
|
1211
|
-
loadBundleItems(store),
|
|
1212
|
-
loadPatchItems(store),
|
|
1213
|
-
loadChannelItems(store),
|
|
1214
|
-
loadReleaseItems(store),
|
|
1215
|
-
loadReleaseCatalogItems(store)
|
|
1216
|
-
]);
|
|
1217
|
-
const bundles = new Map(originalBundleItems.map(({ sk, row }) => [sk, row]));
|
|
1218
|
-
const patches = new Map(originalPatchItems.map(({ sk, row }) => [sk, row]));
|
|
1219
|
-
const channels = new Map(originalChannelItems.map(({ sk, row }) => [sk, row]));
|
|
1220
|
-
const channelNames = new Map(originalChannelItems.map(({ sk, row }) => [row.name, sk]));
|
|
1221
|
-
const releases = new Map(originalReleaseItems.map(({ sk, row }) => [sk, row]));
|
|
1222
|
-
const releaseCatalogs = new Map(originalReleaseCatalogItems.map(({ sk, row }) => [sk, row]));
|
|
1223
|
-
const originalApiKeys = /* @__PURE__ */ new Map();
|
|
1224
|
-
const apiKeys = /* @__PURE__ */ new Map();
|
|
1225
|
-
const apiKeyHashes = /* @__PURE__ */ new Map();
|
|
1226
|
-
const rememberApiKey = (value) => {
|
|
1227
|
-
if (value === null || apiKeys.has(value.row.id)) return;
|
|
1228
|
-
originalApiKeys.set(value.row.id, value);
|
|
1229
|
-
apiKeys.set(value.row.id, value);
|
|
1230
|
-
apiKeyHashes.set(value.row.hash, value.row.id);
|
|
1231
|
-
};
|
|
1232
|
-
const requireReleaseReferences = (row) => {
|
|
1233
|
-
if (!channels.has(row.channel_id)) throw new DynamoDBCommitStateError(`Release "${row.id}" references an invalid channel`);
|
|
1234
|
-
if (row.bundle_id !== null) {
|
|
1235
|
-
const bundle = bundles.get(row.bundle_id);
|
|
1236
|
-
if (bundle === void 0 || bundle.platform !== row.platform) throw new DynamoDBCommitStateError(`Release "${row.id}" references an invalid bundle`);
|
|
1237
|
-
}
|
|
126
|
+
const Key = {
|
|
127
|
+
pk: op.key.pk,
|
|
128
|
+
sk: op.key.sk
|
|
1238
129
|
};
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
reason: "version_conflict"
|
|
1251
|
-
}
|
|
1252
|
-
};
|
|
1253
|
-
}
|
|
1254
|
-
for (const [changeIndex, change] of input.changes.entries()) switch (change.model) {
|
|
1255
|
-
case "channels":
|
|
1256
|
-
if (change.operation === "insert") {
|
|
1257
|
-
if (channelNames.get(change.row.name) !== void 0) break;
|
|
1258
|
-
if (channels.get(change.row.id) !== void 0) throw new DynamoDBCommitStateError(`Channel id "${change.row.id}" already exists`);
|
|
1259
|
-
channels.set(change.row.id, change.row);
|
|
1260
|
-
channelNames.set(change.row.name, change.row.id);
|
|
1261
|
-
} else {
|
|
1262
|
-
const current = channels.get(change.where.id);
|
|
1263
|
-
if (current === void 0) break;
|
|
1264
|
-
if ([...releases.values()].some(({ channel_id }) => channel_id === change.where.id)) return {
|
|
1265
|
-
committed: false,
|
|
1266
|
-
conflict: {
|
|
1267
|
-
changeIndex,
|
|
1268
|
-
reason: "referenced"
|
|
1269
|
-
}
|
|
1270
|
-
};
|
|
1271
|
-
channels.delete(change.where.id);
|
|
1272
|
-
channelNames.delete(current.name);
|
|
1273
|
-
}
|
|
1274
|
-
break;
|
|
1275
|
-
case "bundles":
|
|
1276
|
-
if (change.operation === "insert") {
|
|
1277
|
-
if (bundles.has(change.row.id)) throw new DynamoDBCommitStateError(`Bundle "${change.row.id}" already exists`);
|
|
1278
|
-
bundles.set(change.row.id, change.row);
|
|
1279
|
-
} else {
|
|
1280
|
-
const current = bundles.get(change.where.id);
|
|
1281
|
-
if (current === void 0) {
|
|
1282
|
-
if (change.operation === "update") return {
|
|
1283
|
-
committed: false,
|
|
1284
|
-
conflict: {
|
|
1285
|
-
changeIndex,
|
|
1286
|
-
reason: "not_found"
|
|
1287
|
-
}
|
|
1288
|
-
};
|
|
1289
|
-
break;
|
|
1290
|
-
}
|
|
1291
|
-
if (change.operation === "update") {
|
|
1292
|
-
const row = {
|
|
1293
|
-
...current,
|
|
1294
|
-
...change.update
|
|
1295
|
-
};
|
|
1296
|
-
bundles.set(row.id, row);
|
|
1297
|
-
} else {
|
|
1298
|
-
if ([...releases.values()].some(({ bundle_id }) => bundle_id === change.where.id)) return {
|
|
1299
|
-
committed: false,
|
|
1300
|
-
conflict: {
|
|
1301
|
-
changeIndex,
|
|
1302
|
-
reason: "referenced"
|
|
1303
|
-
}
|
|
1304
|
-
};
|
|
1305
|
-
bundles.delete(change.where.id);
|
|
1306
|
-
for (const [id, patch] of patches) if (patch.bundle_id === change.where.id || patch.base_bundle_id === change.where.id) patches.delete(id);
|
|
1307
|
-
}
|
|
1308
|
-
}
|
|
1309
|
-
break;
|
|
1310
|
-
case "releases":
|
|
1311
|
-
if (change.operation === "insert") {
|
|
1312
|
-
requireReleaseReferences(change.row);
|
|
1313
|
-
if (releases.has(change.row.id)) throw new DynamoDBCommitStateError(`Release "${change.row.id}" already exists`);
|
|
1314
|
-
releases.set(change.row.id, change.row);
|
|
1315
|
-
} else {
|
|
1316
|
-
const current = releases.get(change.where.id);
|
|
1317
|
-
if (current === void 0) {
|
|
1318
|
-
if (change.operation === "update") return {
|
|
1319
|
-
committed: false,
|
|
1320
|
-
conflict: {
|
|
1321
|
-
changeIndex,
|
|
1322
|
-
reason: "not_found"
|
|
1323
|
-
}
|
|
1324
|
-
};
|
|
1325
|
-
break;
|
|
1326
|
-
}
|
|
1327
|
-
if (change.operation === "update") {
|
|
1328
|
-
const row = {
|
|
1329
|
-
...current,
|
|
1330
|
-
...change.update
|
|
1331
|
-
};
|
|
1332
|
-
requireReleaseReferences(row);
|
|
1333
|
-
releases.set(row.id, row);
|
|
1334
|
-
} else releases.delete(change.where.id);
|
|
1335
|
-
}
|
|
1336
|
-
break;
|
|
1337
|
-
case "releaseCatalogs":
|
|
1338
|
-
if (!channels.has(change.row.channel_id)) throw new DynamoDBCommitStateError(`Release catalog "${change.row.scope_key}" references an invalid channel`);
|
|
1339
|
-
releaseCatalogs.set(change.row.scope_key, change.row);
|
|
1340
|
-
break;
|
|
1341
|
-
case "bundlePatches":
|
|
1342
|
-
if (change.operation === "insert") {
|
|
1343
|
-
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`);
|
|
1344
|
-
if (patches.has(change.row.id)) throw new DynamoDBDuplicatePatchError(change.row.id);
|
|
1345
|
-
patches.set(change.row.id, change.row);
|
|
1346
|
-
} else for (const [id, patch] of patches) if (patch.bundle_id === change.where.bundleId) patches.delete(id);
|
|
1347
|
-
break;
|
|
1348
|
-
case "apiKeys": if (change.operation === "insert") {
|
|
1349
|
-
rememberApiKey(await loadApiKeyByHash(store, change.row.hash));
|
|
1350
|
-
if (apiKeyHashes.has(change.row.hash)) break;
|
|
1351
|
-
rememberApiKey(await loadApiKey(store, change.row.id));
|
|
1352
|
-
if (apiKeys.has(change.row.id)) throw new DynamoDBCommitStateError(`API key id "${change.row.id}" already exists`);
|
|
1353
|
-
const inserted = {
|
|
1354
|
-
row: change.row,
|
|
1355
|
-
version: 1
|
|
130
|
+
switch (op.type) {
|
|
131
|
+
case "put": return { Put: item({ Item: toItem(op.key, op.value, op.expiresAt) }) };
|
|
132
|
+
case "delete": return { Delete: item({ Key }) };
|
|
133
|
+
case "check": return { ConditionCheck: item({
|
|
134
|
+
Key,
|
|
135
|
+
ConditionExpression: guard
|
|
136
|
+
}) };
|
|
137
|
+
case "add": {
|
|
138
|
+
const init = {
|
|
139
|
+
...op.init,
|
|
140
|
+
...ttl(op.expiresAt)
|
|
1356
141
|
};
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
}
|
|
1368
|
-
};
|
|
1369
|
-
apiKeys.set(change.where.id, {
|
|
1370
|
-
row: {
|
|
1371
|
-
...current.row,
|
|
1372
|
-
revoked_at_ms: change.update.revokedAtMs
|
|
1373
|
-
},
|
|
1374
|
-
version: current.version
|
|
1375
|
-
});
|
|
1376
|
-
}
|
|
1377
|
-
}
|
|
1378
|
-
const relationCounts = /* @__PURE__ */ new Map();
|
|
1379
|
-
const ownedPatchCounts = /* @__PURE__ */ new Map();
|
|
1380
|
-
for (const patch of patches.values()) {
|
|
1381
|
-
for (const id of /* @__PURE__ */ new Set([patch.bundle_id, patch.base_bundle_id])) relationCounts.set(id, (relationCounts.get(id) ?? 0) + 1);
|
|
1382
|
-
ownedPatchCounts.set(patch.bundle_id, (ownedPatchCounts.get(patch.bundle_id) ?? 0) + 1);
|
|
1383
|
-
}
|
|
1384
|
-
const channelReferenceCounts = /* @__PURE__ */ new Map();
|
|
1385
|
-
for (const release of releases.values()) channelReferenceCounts.set(release.channel_id, (channelReferenceCounts.get(release.channel_id) ?? 0) + 1);
|
|
1386
|
-
const actions = [];
|
|
1387
|
-
const originalChannels = new Map(originalChannelItems.map((item) => [item.sk, item]));
|
|
1388
|
-
for (const original of originalChannelItems) if (!channels.has(original.sk)) actions.push(...deleteChannelActions(store, original));
|
|
1389
|
-
for (const [id, row] of channels) {
|
|
1390
|
-
const original = originalChannels.get(id);
|
|
1391
|
-
const referenceCount = channelReferenceCounts.get(id) ?? 0;
|
|
1392
|
-
if (original === void 0) {
|
|
1393
|
-
actions.push(putChannel(store, row, original, referenceCount));
|
|
1394
|
-
actions.push({ Put: {
|
|
1395
|
-
TableName: store.tableName,
|
|
1396
|
-
Item: {
|
|
1397
|
-
pk: DYNAMODB_CHANNEL_NAME_PARTITION,
|
|
1398
|
-
sk: row.name,
|
|
1399
|
-
channel_id: id
|
|
1400
|
-
},
|
|
1401
|
-
ConditionExpression: "attribute_not_exists(#pk)",
|
|
1402
|
-
ExpressionAttributeNames: { "#pk": "pk" }
|
|
1403
|
-
} });
|
|
1404
|
-
} else {
|
|
1405
|
-
if (!rowsEqual(original.row, row)) throw new DynamoDBCommitStateError(`Channel "${id}" cannot be updated`);
|
|
1406
|
-
const delta = referenceCount - original.reference_count;
|
|
1407
|
-
if (delta !== 0) actions.push(updateChannelReferenceCount(store, original, delta));
|
|
1408
|
-
}
|
|
1409
|
-
}
|
|
1410
|
-
const originalBundles = new Map(originalBundleItems.map((item) => [item.sk, item]));
|
|
1411
|
-
for (const original of originalBundleItems) if (!bundles.has(original.sk)) actions.push(deleteAction(store, original));
|
|
1412
|
-
for (const [id, row] of bundles) {
|
|
1413
|
-
const original = originalBundles.get(id);
|
|
1414
|
-
const relationCount = relationCounts.get(id) ?? 0;
|
|
1415
|
-
const ownedPatchCount = ownedPatchCounts.get(id) ?? 0;
|
|
1416
|
-
if (original === void 0) actions.push(putNewBundle(store, row, relationCount, ownedPatchCount));
|
|
1417
|
-
else if (!rowsEqual(original.row, row)) actions.push(putUpdatedBundle(store, original, row, relationCount, ownedPatchCount));
|
|
1418
|
-
else {
|
|
1419
|
-
const relationDelta = relationCount - original.relation_count;
|
|
1420
|
-
const ownedPatchDelta = ownedPatchCount - original.owned_patch_count;
|
|
1421
|
-
if (relationDelta !== 0 || ownedPatchDelta !== 0) actions.push(updateBundleRelation(store, id, relationDelta, ownedPatchDelta));
|
|
1422
|
-
}
|
|
1423
|
-
}
|
|
1424
|
-
const originalPatches = new Map(originalPatchItems.map((item) => [item.sk, item]));
|
|
1425
|
-
for (const original of originalPatchItems) if (!patches.has(original.sk)) actions.push(deletePatch(store, original));
|
|
1426
|
-
for (const [id, row] of patches) {
|
|
1427
|
-
const original = originalPatches.get(id);
|
|
1428
|
-
if (original === void 0 || !rowsEqual(original.row, row)) actions.push(putPatch(store, row, original));
|
|
1429
|
-
}
|
|
1430
|
-
const originalReleases = new Map(originalReleaseItems.map((item) => [item.sk, item]));
|
|
1431
|
-
for (const original of originalReleaseItems) {
|
|
1432
|
-
if (releases.get(original.sk) !== void 0) continue;
|
|
1433
|
-
actions.push(deleteAction(store, original));
|
|
1434
|
-
actions.push({ Delete: {
|
|
1435
|
-
TableName: store.tableName,
|
|
1436
|
-
Key: {
|
|
1437
|
-
pk: DYNAMODB_RELEASE_ID_PARTITION,
|
|
1438
|
-
sk: original.sk
|
|
1439
|
-
},
|
|
1440
|
-
ConditionExpression: "#scopeKey = :scopeKey",
|
|
1441
|
-
ExpressionAttributeNames: { "#scopeKey": "scope_key" },
|
|
1442
|
-
ExpressionAttributeValues: { ":scopeKey": original.row.scope_key }
|
|
1443
|
-
} });
|
|
1444
|
-
}
|
|
1445
|
-
for (const [id, row] of releases) {
|
|
1446
|
-
const original = originalReleases.get(id);
|
|
1447
|
-
if (original === void 0) {
|
|
1448
|
-
actions.push({ Put: {
|
|
1449
|
-
TableName: store.tableName,
|
|
1450
|
-
Item: boundedDynamoDBMetadataItem(toDynamoDBReleaseItem(row)),
|
|
1451
|
-
ConditionExpression: "attribute_not_exists(#pk)",
|
|
1452
|
-
ExpressionAttributeNames: { "#pk": "pk" }
|
|
1453
|
-
} });
|
|
1454
|
-
actions.push({ Put: {
|
|
1455
|
-
TableName: store.tableName,
|
|
1456
|
-
Item: releaseLocatorItem(row),
|
|
1457
|
-
ConditionExpression: "attribute_not_exists(#pk)",
|
|
1458
|
-
ExpressionAttributeNames: { "#pk": "pk" }
|
|
1459
|
-
} });
|
|
1460
|
-
} else if (!rowsEqual(original.row, row)) {
|
|
1461
|
-
const moved = original.row.scope_key !== row.scope_key;
|
|
1462
|
-
if (moved) actions.push(deleteAction(store, original));
|
|
1463
|
-
actions.push({ Put: {
|
|
1464
|
-
TableName: store.tableName,
|
|
1465
|
-
Item: boundedDynamoDBMetadataItem(toDynamoDBReleaseItem(row, original.version + 1)),
|
|
1466
|
-
ConditionExpression: moved ? "attribute_not_exists(#pk)" : "#version = :version",
|
|
1467
|
-
ExpressionAttributeNames: moved ? { "#pk": "pk" } : { "#version": "version" },
|
|
1468
|
-
...moved ? {} : { ExpressionAttributeValues: { ":version": original.version } }
|
|
1469
|
-
} });
|
|
1470
|
-
if (moved) actions.push({ Put: {
|
|
1471
|
-
TableName: store.tableName,
|
|
1472
|
-
Item: releaseLocatorItem(row),
|
|
1473
|
-
ConditionExpression: "#scopeKey = :scopeKey",
|
|
1474
|
-
ExpressionAttributeNames: { "#scopeKey": "scope_key" },
|
|
1475
|
-
ExpressionAttributeValues: { ":scopeKey": original.row.scope_key }
|
|
1476
|
-
} });
|
|
1477
|
-
}
|
|
1478
|
-
}
|
|
1479
|
-
const originalReleaseCatalogs = new Map(originalReleaseCatalogItems.map((item) => [item.sk, item]));
|
|
1480
|
-
for (const [scopeKey, row] of releaseCatalogs) {
|
|
1481
|
-
const original = originalReleaseCatalogs.get(scopeKey);
|
|
1482
|
-
if (original === void 0 || !rowsEqual(original.row, row)) actions.push({ Put: {
|
|
1483
|
-
TableName: store.tableName,
|
|
1484
|
-
Item: boundedDynamoDBCatalogItem(toDynamoDBReleaseCatalogItem(row, (original?.version ?? 0) + 1)),
|
|
1485
|
-
ConditionExpression: original ? "#version = :version" : "attribute_not_exists(#pk)",
|
|
1486
|
-
ExpressionAttributeNames: original ? { "#version": "version" } : { "#pk": "pk" },
|
|
1487
|
-
...original ? { ExpressionAttributeValues: { ":version": original.version } } : {}
|
|
1488
|
-
} });
|
|
1489
|
-
}
|
|
1490
|
-
const changedReleaseIds = new Set([.../* @__PURE__ */ new Set([...originalReleases.keys(), ...releases.keys()])].filter((id) => {
|
|
1491
|
-
const original = originalReleases.get(id)?.row;
|
|
1492
|
-
const next = releases.get(id);
|
|
1493
|
-
return original === void 0 || next === void 0 || !rowsEqual(original, next);
|
|
1494
|
-
}));
|
|
1495
|
-
const changedCatalogScopes = new Set([...releaseCatalogs].flatMap(([scopeKey, row]) => {
|
|
1496
|
-
const original = originalReleaseCatalogs.get(scopeKey)?.row;
|
|
1497
|
-
return original === void 0 || !rowsEqual(original, row) ? [scopeKey] : [];
|
|
1498
|
-
}));
|
|
1499
|
-
for (const expectation of input.expectations ?? []) {
|
|
1500
|
-
if (expectation.model === "releases" && !changedReleaseIds.has(expectation.id)) {
|
|
1501
|
-
const original = originalReleases.get(expectation.id);
|
|
1502
|
-
actions.push({ ConditionCheck: {
|
|
1503
|
-
TableName: store.tableName,
|
|
1504
|
-
Key: original === void 0 ? {
|
|
1505
|
-
pk: DYNAMODB_RELEASE_ID_PARTITION,
|
|
1506
|
-
sk: expectation.id
|
|
1507
|
-
} : {
|
|
1508
|
-
pk: original.pk,
|
|
1509
|
-
sk: original.sk
|
|
1510
|
-
},
|
|
1511
|
-
ConditionExpression: original === void 0 ? "attribute_not_exists(#pk)" : "#version = :version",
|
|
1512
|
-
ExpressionAttributeNames: original === void 0 ? { "#pk": "pk" } : { "#version": "version" },
|
|
1513
|
-
...original === void 0 ? {} : { ExpressionAttributeValues: { ":version": original.version } }
|
|
1514
|
-
} });
|
|
1515
|
-
}
|
|
1516
|
-
if (expectation.model === "releaseCatalogs" && !changedCatalogScopes.has(expectation.scopeKey)) {
|
|
1517
|
-
const original = originalReleaseCatalogs.get(expectation.scopeKey);
|
|
1518
|
-
actions.push({ ConditionCheck: {
|
|
1519
|
-
TableName: store.tableName,
|
|
1520
|
-
Key: itemKey("release_catalogs", expectation.scopeKey),
|
|
1521
|
-
ConditionExpression: original === void 0 ? "attribute_not_exists(#pk)" : "#version = :version",
|
|
1522
|
-
ExpressionAttributeNames: original === void 0 ? { "#pk": "pk" } : { "#version": "version" },
|
|
1523
|
-
...original === void 0 ? {} : { ExpressionAttributeValues: { ":version": original.version } }
|
|
1524
|
-
} });
|
|
142
|
+
return { Update: item({
|
|
143
|
+
Key,
|
|
144
|
+
UpdateExpression: `SET ${Object.keys({
|
|
145
|
+
...init,
|
|
146
|
+
...op.by
|
|
147
|
+
}).map((column) => {
|
|
148
|
+
const path = name(attribute(column));
|
|
149
|
+
const by = op.by[column];
|
|
150
|
+
if (by === void 0) return `${path} = if_not_exists(${path}, ${value(init[column])})`;
|
|
151
|
+
return `${path} = if_not_exists(${path}, ${value(Number(init[column] ?? 0))}) + ${value(by)}`;
|
|
152
|
+
}).join(", ")}`
|
|
153
|
+
}) };
|
|
1525
154
|
}
|
|
1526
155
|
}
|
|
1527
|
-
const counter = metadataUpdate(store, {
|
|
1528
|
-
bundles: bundles.size - originalBundles.size,
|
|
1529
|
-
bundle_patches: patches.size - originalPatches.size
|
|
1530
|
-
});
|
|
1531
|
-
if (counter !== void 0) actions.push(counter);
|
|
1532
|
-
for (const [id, current] of apiKeys) {
|
|
1533
|
-
const original = originalApiKeys.get(id);
|
|
1534
|
-
if (original !== void 0 && rowsEqual(original.row, current.row)) continue;
|
|
1535
|
-
actions.push({ Put: {
|
|
1536
|
-
TableName: store.tableName,
|
|
1537
|
-
Item: boundedDynamoDBMetadataItem(apiKeyItem(current.row, original === void 0 ? 1 : original.version + 1)),
|
|
1538
|
-
ConditionExpression: original ? "#version = :version" : "attribute_not_exists(#pk)",
|
|
1539
|
-
ExpressionAttributeNames: original ? { "#version": "version" } : { "#pk": "pk" },
|
|
1540
|
-
...original ? { ExpressionAttributeValues: { ":version": original.version } } : {}
|
|
1541
|
-
} });
|
|
1542
|
-
if (original === void 0) actions.push({ Put: {
|
|
1543
|
-
TableName: store.tableName,
|
|
1544
|
-
Item: {
|
|
1545
|
-
pk: DYNAMODB_API_KEY_HASH_PARTITION,
|
|
1546
|
-
sk: current.row.hash,
|
|
1547
|
-
api_key_id: current.row.id
|
|
1548
|
-
},
|
|
1549
|
-
ConditionExpression: "attribute_not_exists(#pk)",
|
|
1550
|
-
ExpressionAttributeNames: { "#pk": "pk" }
|
|
1551
|
-
} });
|
|
1552
|
-
}
|
|
1553
|
-
if (actions.length > 0) await commitDynamoDBTransaction(store, actions);
|
|
1554
|
-
return { committed: true };
|
|
1555
|
-
};
|
|
1556
|
-
const isDynamoDBTransactionConflict = (error) => typeof error === "object" && error !== null && Reflect.get(error, "name") === "TransactionCanceledException";
|
|
1557
|
-
const createDynamoDBCommit = (store) => async (input) => {
|
|
1558
|
-
try {
|
|
1559
|
-
return await compileAndCommitDynamoDBChanges(store, input);
|
|
1560
|
-
} catch (error) {
|
|
1561
|
-
if (!isDynamoDBTransactionConflict(error)) throw error;
|
|
1562
|
-
return compileAndCommitDynamoDBChanges(store, input);
|
|
1563
|
-
}
|
|
1564
156
|
};
|
|
1565
|
-
|
|
1566
|
-
const
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
};
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
157
|
+
/** Errors, and cancellation reasons, that a retry of the whole write can clear. */
|
|
158
|
+
const TRANSIENT = /* @__PURE__ */ new Set([
|
|
159
|
+
"TransactionConflict",
|
|
160
|
+
"ThrottlingError",
|
|
161
|
+
"ProvisionedThroughputExceeded",
|
|
162
|
+
"RequestLimitExceeded",
|
|
163
|
+
"TransactionInProgressException",
|
|
164
|
+
"ProvisionedThroughputExceededException",
|
|
165
|
+
"ThrottlingException",
|
|
166
|
+
"InternalServerError"
|
|
167
|
+
]);
|
|
168
|
+
/** A missing table reads as one to the schema fence, like SQL's 42P01. */
|
|
169
|
+
const missingTable = (error) => {
|
|
170
|
+
throw nameOf(error) === "ResourceNotFoundException" ? Object.assign(new Error(error.message, { cause: error }), { code: "42P01" }) : error;
|
|
171
|
+
};
|
|
172
|
+
/**
|
|
173
|
+
* The key-value store over one DynamoDB table keyed by `pk` and `sk`, with
|
|
174
|
+
* no secondary index. Reads are strongly consistent; a write is one
|
|
175
|
+
* `TransactWriteItems` with a client request token.
|
|
176
|
+
*/
|
|
177
|
+
const createDynamoDBStore = ({ client, tableName, nativePageSize }) => {
|
|
178
|
+
const documents = _aws_sdk_lib_dynamodb.DynamoDBDocumentClient.from(client, { marshallOptions: { removeUndefinedValues: true } });
|
|
1585
179
|
return {
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
const
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
};
|
|
1603
|
-
const
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
type: row.type,
|
|
1609
|
-
fromBundleId: row.from_bundle_id
|
|
1610
|
-
} : {
|
|
1611
|
-
type: row.type,
|
|
1612
|
-
toBundleId: row.to_bundle_id
|
|
1613
|
-
}
|
|
1614
|
-
}),
|
|
1615
|
-
sk: insightsSortKey(row),
|
|
1616
|
-
version: 1,
|
|
1617
|
-
row
|
|
1618
|
-
});
|
|
1619
|
-
const toInsightsEventItem = (row) => {
|
|
1620
|
-
const orderKey = insightsSortKey(row);
|
|
1621
|
-
return boundedDynamoDBMetadataItem({
|
|
1622
|
-
pk: DYNAMODB_INSIGHTS_PARTITION,
|
|
1623
|
-
sk: orderKey,
|
|
1624
|
-
version: 1,
|
|
1625
|
-
row,
|
|
1626
|
-
...(0, _hot_updater_plugin_core.isInsightsMovementEvent)(row) ? {
|
|
1627
|
-
gsi1pk: insightsMovementPartition(row.install_id),
|
|
1628
|
-
gsi1sk: orderKey
|
|
1629
|
-
} : {}
|
|
1630
|
-
});
|
|
1631
|
-
};
|
|
1632
|
-
const toInsightsInstallationItem = (row) => boundedDynamoDBMetadataItem({
|
|
1633
|
-
pk: DYNAMODB_INSIGHTS_INSTALLATIONS_PARTITION,
|
|
1634
|
-
sk: row.install_id,
|
|
1635
|
-
order_key: insightsSortKey(row),
|
|
1636
|
-
version: 1,
|
|
1637
|
-
row
|
|
1638
|
-
});
|
|
1639
|
-
const toInsightsUserItem = (row) => boundedDynamoDBMetadataItem({
|
|
1640
|
-
pk: insightsUserPartition(row.user_id),
|
|
1641
|
-
sk: row.install_id,
|
|
1642
|
-
order_key: insightsSortKey(row),
|
|
1643
|
-
version: 1,
|
|
1644
|
-
row
|
|
1645
|
-
});
|
|
1646
|
-
const parseInsightsInstallationItem = (value, partition) => {
|
|
1647
|
-
const row = value.row;
|
|
1648
|
-
if (value.pk !== partition || typeof value.sk !== "string" || typeof value.order_key !== "string" || value.version !== 1 || !isInsightsInstallationRow(row) || value.sk !== row.install_id || value.order_key !== insightsSortKey(row)) throw new DynamoDBStoredItemError();
|
|
1649
|
-
return value;
|
|
1650
|
-
};
|
|
1651
|
-
const loadInsightsInstallationItem = async (store, installId) => {
|
|
1652
|
-
const { Item } = await store.client.send(new _aws_sdk_lib_dynamodb.GetCommand({
|
|
1653
|
-
TableName: store.tableName,
|
|
1654
|
-
Key: {
|
|
1655
|
-
pk: DYNAMODB_INSIGHTS_INSTALLATIONS_PARTITION,
|
|
1656
|
-
sk: installId
|
|
180
|
+
id: "dynamoDB",
|
|
181
|
+
limits: DYNAMODB_LIMITS,
|
|
182
|
+
async get(keys) {
|
|
183
|
+
const found = /* @__PURE__ */ new Map();
|
|
184
|
+
const unique = [...new Map(keys.map((key) => [idOf(key), key])).values()];
|
|
185
|
+
for (let at = 0; at < unique.length; at += GET_BATCH) {
|
|
186
|
+
let pending = unique.slice(at, at + GET_BATCH).map(({ pk, sk }) => ({
|
|
187
|
+
pk,
|
|
188
|
+
sk
|
|
189
|
+
}));
|
|
190
|
+
for (let round = 0; pending.length > 0; round += 1) {
|
|
191
|
+
if (round > 0) await sleep(2 ** round * 10);
|
|
192
|
+
const RequestItems = { [tableName]: {
|
|
193
|
+
Keys: pending,
|
|
194
|
+
ConsistentRead: true
|
|
195
|
+
} };
|
|
196
|
+
const result = await documents.send(new _aws_sdk_lib_dynamodb.BatchGetCommand({ RequestItems })).catch(missingTable);
|
|
197
|
+
for (const item of result.Responses?.[tableName] ?? []) found.set(idOf(item), toRow(item));
|
|
198
|
+
pending = result.UnprocessedKeys?.[tableName]?.Keys ?? [];
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return keys.map((key) => found.get(idOf(key)) ?? null);
|
|
1657
202
|
},
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
}
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
for (let attempt = 0; attempt < DYNAMODB_INSIGHTS_RECORD_ATTEMPTS; attempt++) {
|
|
1671
|
-
const { Item: accepted } = await store.client.send(new _aws_sdk_lib_dynamodb.GetCommand({
|
|
1672
|
-
TableName: store.tableName,
|
|
1673
|
-
Key: identityKey,
|
|
1674
|
-
ConsistentRead: true
|
|
1675
|
-
}));
|
|
1676
|
-
if (accepted !== void 0) return;
|
|
1677
|
-
const current = await loadInsightsInstallationItem(store, row.install_id);
|
|
1678
|
-
const actions = [
|
|
1679
|
-
{ Put: {
|
|
1680
|
-
TableName: store.tableName,
|
|
1681
|
-
Item: identityKey,
|
|
1682
|
-
ConditionExpression: "attribute_not_exists(#pk)",
|
|
1683
|
-
ExpressionAttributeNames: { "#pk": "pk" }
|
|
1684
|
-
} },
|
|
1685
|
-
{ Put: {
|
|
1686
|
-
TableName: store.tableName,
|
|
1687
|
-
Item: eventItem
|
|
1688
|
-
} },
|
|
1689
|
-
...bundleItem === null ? [] : [{ Put: {
|
|
1690
|
-
TableName: store.tableName,
|
|
1691
|
-
Item: bundleItem
|
|
1692
|
-
} }]
|
|
1693
|
-
];
|
|
1694
|
-
if (current === null || advancesInsightsInstallation(next, current.row)) {
|
|
1695
|
-
actions.push({ Put: {
|
|
1696
|
-
TableName: store.tableName,
|
|
1697
|
-
Item: toInsightsInstallationItem(next),
|
|
1698
|
-
ConditionExpression: current === null ? "attribute_not_exists(#pk)" : "#orderKey = :currentOrderKey",
|
|
1699
|
-
ExpressionAttributeNames: current === null ? { "#pk": "pk" } : { "#orderKey": "order_key" },
|
|
1700
|
-
...current === null ? {} : { ExpressionAttributeValues: { ":currentOrderKey": current.order_key } }
|
|
1701
|
-
} });
|
|
1702
|
-
if (current !== null && current.row.user_id !== null && current.row.user_id !== next.user_id) actions.push({ Delete: {
|
|
1703
|
-
TableName: store.tableName,
|
|
1704
|
-
Key: {
|
|
1705
|
-
pk: insightsUserPartition(current.row.user_id),
|
|
1706
|
-
sk: current.row.install_id
|
|
203
|
+
/** Plain BatchWriteItem deletes: 1 write unit per KB each, not a transaction's 2. */
|
|
204
|
+
async deleteConsumed(keys) {
|
|
205
|
+
for (let at = 0; at < keys.length; at += WRITE_BATCH) {
|
|
206
|
+
let pending = keys.slice(at, at + WRITE_BATCH).map(({ pk, sk }) => ({ DeleteRequest: { Key: {
|
|
207
|
+
pk,
|
|
208
|
+
sk
|
|
209
|
+
} } }));
|
|
210
|
+
for (let round = 0; pending.length > 0; round += 1) {
|
|
211
|
+
if (round > 0) await sleep(2 ** round * 10);
|
|
212
|
+
const RequestItems = { [tableName]: pending };
|
|
213
|
+
const { UnprocessedItems } = await documents.send(new _aws_sdk_lib_dynamodb.BatchWriteCommand({ RequestItems })).catch(missingTable);
|
|
214
|
+
pending = UnprocessedItems?.[tableName] ?? [];
|
|
1707
215
|
}
|
|
1708
|
-
} });
|
|
1709
|
-
if (next.user_id !== null) actions.push({ Put: {
|
|
1710
|
-
TableName: store.tableName,
|
|
1711
|
-
Item: toInsightsUserItem({
|
|
1712
|
-
...next,
|
|
1713
|
-
user_id: next.user_id
|
|
1714
|
-
})
|
|
1715
|
-
} });
|
|
1716
|
-
}
|
|
1717
|
-
try {
|
|
1718
|
-
await commitDynamoDBTransaction(store, actions);
|
|
1719
|
-
return;
|
|
1720
|
-
} catch (error) {
|
|
1721
|
-
if (!isDynamoDBTransactionConflict(error)) throw error;
|
|
1722
|
-
const { Item } = await store.client.send(new _aws_sdk_lib_dynamodb.GetCommand({
|
|
1723
|
-
TableName: store.tableName,
|
|
1724
|
-
Key: identityKey,
|
|
1725
|
-
ConsistentRead: true
|
|
1726
|
-
}));
|
|
1727
|
-
if (Item !== void 0) return;
|
|
1728
|
-
if (attempt === 2) throw error;
|
|
1729
|
-
}
|
|
1730
|
-
}
|
|
1731
|
-
};
|
|
1732
|
-
const insightsEventRange = (input) => {
|
|
1733
|
-
const upperExclusive = input.after ? `${timestampSortKey(input.after.receivedAtMs)}#${input.after.id}` : `${timestampSortKey(input.beforeReceivedAtMs)}#`;
|
|
1734
|
-
const upper = `${upperExclusive.slice(0, -1)}${String.fromCharCode(upperExclusive.charCodeAt(upperExclusive.length - 1) - 1)}~`;
|
|
1735
|
-
const movement = input.filter.kind === "installationMovement";
|
|
1736
|
-
const partition = movement ? insightsMovementPartition(input.filter.installId) : input.filter.kind === "bundle" ? insightsBundlePartition(input.filter) : DYNAMODB_INSIGHTS_PARTITION;
|
|
1737
|
-
return {
|
|
1738
|
-
partition,
|
|
1739
|
-
query: {
|
|
1740
|
-
...movement ? { IndexName: DYNAMODB_UPDATE_INDEX_NAME } : { ConsistentRead: true },
|
|
1741
|
-
KeyConditionExpression: "#partition = :partition AND #order BETWEEN :since AND :upper",
|
|
1742
|
-
ExpressionAttributeNames: {
|
|
1743
|
-
"#partition": movement ? "gsi1pk" : "pk",
|
|
1744
|
-
"#order": movement ? "gsi1sk" : "sk"
|
|
1745
|
-
},
|
|
1746
|
-
ExpressionAttributeValues: {
|
|
1747
|
-
":partition": partition,
|
|
1748
|
-
":since": `${timestampSortKey(input.sinceMs ?? 0)}#`,
|
|
1749
|
-
":upper": upper
|
|
1750
216
|
}
|
|
1751
|
-
}
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
const
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
do {
|
|
1762
|
-
const page = await store.client.send(new _aws_sdk_lib_dynamodb.QueryCommand({
|
|
1763
|
-
TableName: store.tableName,
|
|
1764
|
-
...range.query,
|
|
1765
|
-
ExclusiveStartKey: exclusiveStartKey,
|
|
1766
|
-
Limit: input.limit - rows.length,
|
|
1767
|
-
ScanIndexForward: false
|
|
1768
|
-
}));
|
|
1769
|
-
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));
|
|
1770
|
-
exclusiveStartKey = page.LastEvaluatedKey;
|
|
1771
|
-
} while (rows.length < input.limit && exclusiveStartKey !== void 0);
|
|
1772
|
-
return rows;
|
|
1773
|
-
},
|
|
1774
|
-
async findInstallations(input) {
|
|
1775
|
-
if ("installId" in input) {
|
|
1776
|
-
const stored = await loadInsightsInstallationItem(store, input.installId);
|
|
1777
|
-
return stored === null ? [] : [stored.row];
|
|
1778
|
-
}
|
|
1779
|
-
const partition = insightsUserPartition(input.userId);
|
|
1780
|
-
const rows = [];
|
|
1781
|
-
let exclusiveStartKey;
|
|
1782
|
-
do {
|
|
1783
|
-
const page = await store.client.send(new _aws_sdk_lib_dynamodb.QueryCommand({
|
|
1784
|
-
TableName: store.tableName,
|
|
1785
|
-
ConsistentRead: true,
|
|
1786
|
-
ExclusiveStartKey: exclusiveStartKey,
|
|
1787
|
-
KeyConditionExpression: input.afterInstallId === void 0 ? "#pk = :pk" : "#pk = :pk AND #sk > :after",
|
|
1788
|
-
ExpressionAttributeNames: {
|
|
1789
|
-
"#pk": "pk",
|
|
1790
|
-
...input.afterInstallId === void 0 ? {} : { "#sk": "sk" }
|
|
1791
|
-
},
|
|
217
|
+
},
|
|
218
|
+
async query({ pk, gte, lt, lte, order, limit, after }) {
|
|
219
|
+
let condition = "pk = :pk";
|
|
220
|
+
const between = gte !== void 0 && lt !== void 0;
|
|
221
|
+
if (between) condition += " AND sk BETWEEN :gte AND :lte";
|
|
222
|
+
else if (gte !== void 0) condition += " AND sk >= :gte";
|
|
223
|
+
else if (lt !== void 0) condition += " AND sk < :lt";
|
|
224
|
+
const result = await documents.send(new _aws_sdk_lib_dynamodb.QueryCommand({
|
|
225
|
+
TableName: tableName,
|
|
226
|
+
KeyConditionExpression: condition,
|
|
1792
227
|
ExpressionAttributeValues: {
|
|
1793
|
-
":pk":
|
|
1794
|
-
|
|
228
|
+
":pk": pk,
|
|
229
|
+
":gte": gte,
|
|
230
|
+
...between ? { ":lte": lte ?? lt } : { ":lt": lt }
|
|
1795
231
|
},
|
|
1796
|
-
|
|
1797
|
-
ScanIndexForward: true
|
|
1798
|
-
}));
|
|
1799
|
-
for (const item of (page.Items ?? []).slice(0, input.limit - rows.length)) {
|
|
1800
|
-
const stored = parseInsightsInstallationItem(item, partition);
|
|
1801
|
-
const current = await loadInsightsInstallationItem(store, stored.row.install_id);
|
|
1802
|
-
if (current?.row.user_id === input.userId) rows.push(current.row);
|
|
1803
|
-
}
|
|
1804
|
-
exclusiveStartKey = page.LastEvaluatedKey;
|
|
1805
|
-
} while (rows.length < input.limit && exclusiveStartKey !== void 0);
|
|
1806
|
-
return rows;
|
|
1807
|
-
},
|
|
1808
|
-
async countInstallations(input) {
|
|
1809
|
-
let count = 0;
|
|
1810
|
-
let exclusiveStartKey;
|
|
1811
|
-
do {
|
|
1812
|
-
const page = await store.client.send(new _aws_sdk_lib_dynamodb.QueryCommand({
|
|
1813
|
-
TableName: store.tableName,
|
|
232
|
+
ScanIndexForward: order === "asc",
|
|
1814
233
|
ConsistentRead: true,
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
},
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
":platform": input.platform,
|
|
1829
|
-
":channel": input.channel,
|
|
1830
|
-
":since": input.sinceMs,
|
|
1831
|
-
...input.bundleId === void 0 ? {} : { ":bundle": input.bundleId }
|
|
1832
|
-
},
|
|
1833
|
-
Select: "COUNT",
|
|
1834
|
-
ScanIndexForward: true
|
|
1835
|
-
}));
|
|
1836
|
-
count += page.Count ?? 0;
|
|
1837
|
-
exclusiveStartKey = page.LastEvaluatedKey;
|
|
1838
|
-
} while (exclusiveStartKey !== void 0);
|
|
1839
|
-
return count;
|
|
1840
|
-
},
|
|
1841
|
-
async countEvents(input) {
|
|
1842
|
-
if (input.sinceMs === input.beforeReceivedAtMs) return 0;
|
|
1843
|
-
const { query } = insightsEventRange({
|
|
1844
|
-
...input,
|
|
1845
|
-
filter: {
|
|
1846
|
-
kind: "bundle",
|
|
1847
|
-
...input.filter
|
|
1848
|
-
},
|
|
1849
|
-
limit: 1
|
|
1850
|
-
});
|
|
1851
|
-
let count = 0;
|
|
1852
|
-
let exclusiveStartKey;
|
|
1853
|
-
do {
|
|
1854
|
-
const page = await store.client.send(new _aws_sdk_lib_dynamodb.QueryCommand({
|
|
1855
|
-
TableName: store.tableName,
|
|
1856
|
-
...query,
|
|
1857
|
-
ExclusiveStartKey: exclusiveStartKey,
|
|
1858
|
-
Select: "COUNT"
|
|
1859
|
-
}));
|
|
1860
|
-
count += page.Count ?? 0;
|
|
1861
|
-
exclusiveStartKey = page.LastEvaluatedKey;
|
|
1862
|
-
} while (exclusiveStartKey !== void 0);
|
|
1863
|
-
return count;
|
|
1864
|
-
}
|
|
1865
|
-
});
|
|
1866
|
-
const apiKeyItem = (row, version) => ({
|
|
1867
|
-
pk: DYNAMODB_API_KEY_PARTITION,
|
|
1868
|
-
sk: row.id,
|
|
1869
|
-
version,
|
|
1870
|
-
row
|
|
1871
|
-
});
|
|
1872
|
-
const loadApiKey = async (store, id) => {
|
|
1873
|
-
const result = await store.client.send(new _aws_sdk_lib_dynamodb.GetCommand({
|
|
1874
|
-
TableName: store.tableName,
|
|
1875
|
-
Key: {
|
|
1876
|
-
pk: DYNAMODB_API_KEY_PARTITION,
|
|
1877
|
-
sk: id
|
|
234
|
+
Limit: Math.min(limit, nativePageSize ?? limit),
|
|
235
|
+
ExclusiveStartKey: after === void 0 ? void 0 : {
|
|
236
|
+
pk,
|
|
237
|
+
sk: after
|
|
238
|
+
}
|
|
239
|
+
})).catch(missingTable);
|
|
240
|
+
return {
|
|
241
|
+
items: (result.Items ?? []).map((item) => ({
|
|
242
|
+
sk: item.sk,
|
|
243
|
+
value: toRow(item)
|
|
244
|
+
})),
|
|
245
|
+
more: result.LastEvaluatedKey !== void 0
|
|
246
|
+
};
|
|
1878
247
|
},
|
|
1879
|
-
|
|
1880
|
-
}));
|
|
1881
|
-
return result.Item === void 0 ? null : parseOfficialRowItem(result.Item, DYNAMODB_API_KEY_PARTITION, isApiKeyRow);
|
|
1882
|
-
};
|
|
1883
|
-
const createDynamoDBApiKeyTable = (store) => {
|
|
1884
|
-
const findByHash = async (hash) => {
|
|
1885
|
-
const lookup = await store.client.send(new _aws_sdk_lib_dynamodb.GetCommand({
|
|
1886
|
-
TableName: store.tableName,
|
|
1887
|
-
Key: {
|
|
1888
|
-
pk: DYNAMODB_API_KEY_HASH_PARTITION,
|
|
1889
|
-
sk: hash
|
|
1890
|
-
},
|
|
1891
|
-
ConsistentRead: true
|
|
1892
|
-
}));
|
|
1893
|
-
const id = lookup.Item?.api_key_id;
|
|
1894
|
-
if (lookup.Item === void 0) return null;
|
|
1895
|
-
if (typeof id !== "string") throw new DynamoDBStoredItemError();
|
|
1896
|
-
return (await loadApiKey(store, id))?.row ?? null;
|
|
1897
|
-
};
|
|
1898
|
-
return {
|
|
1899
|
-
async create(row) {
|
|
1900
|
-
if (await findByHash(row.hash) !== null) return "existing";
|
|
248
|
+
async write(ops) {
|
|
1901
249
|
try {
|
|
1902
|
-
await
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
} }, { Put: {
|
|
1908
|
-
TableName: store.tableName,
|
|
1909
|
-
Item: {
|
|
1910
|
-
pk: DYNAMODB_API_KEY_HASH_PARTITION,
|
|
1911
|
-
sk: row.hash,
|
|
1912
|
-
api_key_id: row.id
|
|
1913
|
-
},
|
|
1914
|
-
ConditionExpression: "attribute_not_exists(#pk)",
|
|
1915
|
-
ExpressionAttributeNames: { "#pk": "pk" }
|
|
1916
|
-
} }]);
|
|
1917
|
-
return "created";
|
|
250
|
+
await documents.send(new _aws_sdk_lib_dynamodb.TransactWriteCommand({
|
|
251
|
+
TransactItems: ops.map((op) => toTransactItem(tableName, op)),
|
|
252
|
+
ClientRequestToken: crypto.randomUUID()
|
|
253
|
+
}));
|
|
254
|
+
return { ok: true };
|
|
1918
255
|
} catch (error) {
|
|
1919
|
-
|
|
1920
|
-
|
|
256
|
+
const { CancellationReasons = [] } = error;
|
|
257
|
+
const codes = CancellationReasons.map(({ Code }) => Code);
|
|
258
|
+
const failed = codes.indexOf("ConditionalCheckFailed");
|
|
259
|
+
if (failed !== -1) return {
|
|
260
|
+
ok: false,
|
|
261
|
+
failedOp: failed
|
|
262
|
+
};
|
|
263
|
+
return [nameOf(error), ...codes].some((code) => TRANSIENT.has(code)) ? {
|
|
264
|
+
ok: false,
|
|
265
|
+
retry: true
|
|
266
|
+
} : missingTable(error);
|
|
1921
267
|
}
|
|
1922
268
|
},
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
269
|
+
migrations: {
|
|
270
|
+
/**
|
|
271
|
+
* Creates the table for local and test runs, and turns on its TTL,
|
|
272
|
+
* which every table the store runs on needs; `hot-updater init`
|
|
273
|
+
* creates it in AWS.
|
|
274
|
+
*/
|
|
275
|
+
async apply() {
|
|
276
|
+
try {
|
|
277
|
+
await client.send(new _aws_sdk_client_dynamodb.DescribeTableCommand({ TableName: tableName }));
|
|
278
|
+
} catch (error) {
|
|
279
|
+
if (nameOf(error) !== "ResourceNotFoundException") throw error;
|
|
280
|
+
await client.send(new _aws_sdk_client_dynamodb.CreateTableCommand({
|
|
281
|
+
TableName: tableName,
|
|
282
|
+
AttributeDefinitions: [{
|
|
283
|
+
AttributeName: "pk",
|
|
284
|
+
AttributeType: "S"
|
|
285
|
+
}, {
|
|
286
|
+
AttributeName: "sk",
|
|
287
|
+
AttributeType: "S"
|
|
288
|
+
}],
|
|
289
|
+
KeySchema: [{
|
|
290
|
+
AttributeName: "pk",
|
|
291
|
+
KeyType: "HASH"
|
|
292
|
+
}, {
|
|
293
|
+
AttributeName: "sk",
|
|
294
|
+
KeyType: "RANGE"
|
|
295
|
+
}],
|
|
296
|
+
BillingMode: "PAY_PER_REQUEST"
|
|
1935
297
|
}));
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
298
|
+
await (0, _aws_sdk_client_dynamodb.waitUntilTableExists)({
|
|
299
|
+
client,
|
|
300
|
+
maxWaitTime: 120,
|
|
301
|
+
minDelay: 1,
|
|
302
|
+
maxDelay: 1
|
|
303
|
+
}, { TableName: tableName });
|
|
304
|
+
}
|
|
305
|
+
const { TimeToLiveDescription: ttl } = await client.send(new _aws_sdk_client_dynamodb.DescribeTimeToLiveCommand({ TableName: tableName }));
|
|
306
|
+
if (ttl?.TimeToLiveStatus?.startsWith("ENABL")) return;
|
|
307
|
+
const input = {
|
|
308
|
+
TableName: tableName,
|
|
309
|
+
TimeToLiveSpecification: {
|
|
310
|
+
AttributeName: DYNAMODB_TTL_ATTRIBUTE,
|
|
311
|
+
Enabled: true
|
|
312
|
+
}
|
|
1947
313
|
};
|
|
1948
|
-
await
|
|
1949
|
-
|
|
1950
|
-
Item: boundedDynamoDBMetadataItem(apiKeyItem(row, current.version + 1)),
|
|
1951
|
-
ConditionExpression: "#version = :currentVersion",
|
|
1952
|
-
ExpressionAttributeNames: { "#version": "version" },
|
|
1953
|
-
ExpressionAttributeValues: { ":currentVersion": current.version }
|
|
1954
|
-
}));
|
|
1955
|
-
return row;
|
|
1956
|
-
}
|
|
314
|
+
await client.send(new _aws_sdk_client_dynamodb.UpdateTimeToLiveCommand(input));
|
|
315
|
+
} }
|
|
1957
316
|
};
|
|
1958
317
|
};
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
const
|
|
1964
|
-
|
|
1965
|
-
region: clientConfig.region
|
|
1966
|
-
}) : null;
|
|
1967
|
-
const store = {
|
|
318
|
+
//#endregion
|
|
319
|
+
//#region src/dynamoDB.ts
|
|
320
|
+
/** The DynamoDB client ignores the CloudFront and batching settings. */
|
|
321
|
+
const adapterOf = ({ tableName, ...clientConfig }) => {
|
|
322
|
+
const client = new _aws_sdk_client_dynamodb.DynamoDBClient(clientConfig);
|
|
323
|
+
const store = createDynamoDBStore({
|
|
1968
324
|
client,
|
|
1969
325
|
tableName
|
|
326
|
+
});
|
|
327
|
+
return {
|
|
328
|
+
client,
|
|
329
|
+
adapter: (0, _hot_updater_plugin_core.createKvAdapter)({ store })
|
|
1970
330
|
};
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
331
|
+
};
|
|
332
|
+
/**
|
|
333
|
+
* Creates the table when it is missing and writes the schema settings of
|
|
334
|
+
* core and `plugins`, the plugins the server runs, which the database checks
|
|
335
|
+
* before its first read. `hot-updater init` runs it in AWS with the managed
|
|
336
|
+
* server's plugins.
|
|
337
|
+
*/
|
|
338
|
+
const migrateDynamoDB = (config, plugins = []) => {
|
|
339
|
+
const { client, adapter } = adapterOf(config);
|
|
340
|
+
return (0, _hot_updater_plugin_core.migrateCoreSchema)(adapter, "dynamoDB", plugins).finally(() => client.destroy());
|
|
341
|
+
};
|
|
342
|
+
/**
|
|
343
|
+
* Hot Updater's database on one DynamoDB table, through the storage engine.
|
|
344
|
+
* With a distribution, it gives core the CloudFront invalidation core runs
|
|
345
|
+
* after a write that changes what the update-check routes answer.
|
|
346
|
+
*/
|
|
347
|
+
const dynamoDB = (config) => {
|
|
348
|
+
const { client, adapter } = adapterOf(config);
|
|
349
|
+
const cloudFront = createUpdateRouteInvalidation(config);
|
|
350
|
+
return {
|
|
351
|
+
...(0, _hot_updater_plugin_core.createEngineDatabase)({
|
|
352
|
+
name: "dynamoDB",
|
|
353
|
+
adapter,
|
|
354
|
+
onCachedRoutesChange: cloudFront?.invalidate,
|
|
355
|
+
aggregateBatching: config.aggregateBatching ?? {}
|
|
356
|
+
}),
|
|
357
|
+
async dispose() {
|
|
358
|
+
await adapter.dispose?.();
|
|
1987
359
|
client.destroy();
|
|
1988
360
|
cloudFront?.destroy();
|
|
1989
361
|
}
|
|
1990
|
-
}
|
|
1991
|
-
return (0, _hot_updater_plugin_core.createDatabasePlugin)({
|
|
1992
|
-
name: "dynamoDB",
|
|
1993
|
-
models: {
|
|
1994
|
-
...adapter.models,
|
|
1995
|
-
bundlePatches: createDynamoDBBundlePatchTable(store, DYNAMODB_UPDATE_INDEX_NAME),
|
|
1996
|
-
insights: createDynamoDBInsightsTable(store),
|
|
1997
|
-
apiKeys: createDynamoDBApiKeyTable(store)
|
|
1998
|
-
},
|
|
1999
|
-
async commit(input) {
|
|
2000
|
-
const result = await adapter.commit(input);
|
|
2001
|
-
if (result.committed && input.changes.some(({ model }) => model === "bundles" || model === "bundlePatches")) await invalidateUpdateRoutes();
|
|
2002
|
-
return result;
|
|
2003
|
-
},
|
|
2004
|
-
dispose: adapter.dispose
|
|
2005
|
-
});
|
|
362
|
+
};
|
|
2006
363
|
};
|
|
2007
364
|
//#endregion
|
|
2008
365
|
//#region src/runtimeAwsConfig.ts
|
|
@@ -2092,6 +449,21 @@ function s3Storage(config) {
|
|
|
2092
449
|
const value = [normalizedBasePath, normalizedPrefix].filter(Boolean).join("/");
|
|
2093
450
|
return value ? `${value}/` : "";
|
|
2094
451
|
};
|
|
452
|
+
/**
|
|
453
|
+
* The URI `put` returns for the key, or null for a key no `put` writes,
|
|
454
|
+
* such as a folder marker that ends in "/".
|
|
455
|
+
*/
|
|
456
|
+
const getListedStorageUri = (key) => {
|
|
457
|
+
try {
|
|
458
|
+
return (0, _hot_updater_plugin_core.createStorageUri)({
|
|
459
|
+
bucket: bucketName,
|
|
460
|
+
key,
|
|
461
|
+
protocol: "s3"
|
|
462
|
+
});
|
|
463
|
+
} catch {
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
};
|
|
2095
467
|
const getRelativeKey = (key) => {
|
|
2096
468
|
if (!normalizedBasePath) return key;
|
|
2097
469
|
const basePrefix = `${normalizedBasePath}/`;
|
|
@@ -2102,7 +474,7 @@ function s3Storage(config) {
|
|
|
2102
474
|
if (parsed.bucket !== bucketName) throw new Error(`Bucket name mismatch: expected "${bucketName}", but found "${parsed.bucket}".`);
|
|
2103
475
|
return parsed;
|
|
2104
476
|
};
|
|
2105
|
-
return (0, _hot_updater_plugin_core.
|
|
477
|
+
return (0, _hot_updater_plugin_core.createStorageAdapter)({
|
|
2106
478
|
name: "s3Storage",
|
|
2107
479
|
protocol: "s3",
|
|
2108
480
|
async listObjects(prefix) {
|
|
@@ -2116,11 +488,13 @@ function s3Storage(config) {
|
|
|
2116
488
|
}));
|
|
2117
489
|
for (const object of response.Contents ?? []) {
|
|
2118
490
|
if (!object.Key) continue;
|
|
491
|
+
const storageUri = getListedStorageUri(object.Key);
|
|
492
|
+
if (storageUri === null) continue;
|
|
2119
493
|
objects.push({
|
|
2120
494
|
key: getRelativeKey(object.Key),
|
|
2121
495
|
lastModifiedAt: object.LastModified,
|
|
2122
496
|
size: object.Size ?? 0,
|
|
2123
|
-
storageUri
|
|
497
|
+
storageUri
|
|
2124
498
|
});
|
|
2125
499
|
}
|
|
2126
500
|
continuationToken = response.NextContinuationToken;
|
|
@@ -2207,7 +581,15 @@ function s3Storage(config) {
|
|
|
2207
581
|
});
|
|
2208
582
|
}
|
|
2209
583
|
//#endregion
|
|
2210
|
-
|
|
584
|
+
//#region src/plugins.ts
|
|
585
|
+
/**
|
|
586
|
+
* The plugins the managed AWS server runs: Insights, and API keys on every
|
|
587
|
+
* client route. Its Lambda@Edge function imports this list.
|
|
588
|
+
*/
|
|
589
|
+
const plugins = [(0, _hot_updater_server_plugins_insights.insights)(), (0, _hot_updater_server_plugins_api_keys.apiKeys)()];
|
|
590
|
+
//#endregion
|
|
2211
591
|
exports.cloudFrontDownloadUrl = cloudFrontDownloadUrl;
|
|
2212
592
|
exports.dynamoDB = dynamoDB;
|
|
593
|
+
exports.migrateDynamoDB = migrateDynamoDB;
|
|
594
|
+
exports.plugins = plugins;
|
|
2213
595
|
exports.s3Storage = s3Storage;
|