@hot-updater/azure 1.2.0-rc.4 → 1.2.0-rc.6
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/functions/index.mjs +41 -25
- package/dist/index.cjs +40 -24
- package/dist/index.d.cts +14 -2
- package/dist/index.d.mts +14 -2
- package/dist/index.mjs +40 -24
- package/dist/init/index.cjs +50 -24
- package/dist/init/index.d.cts +2 -1
- package/dist/init/index.d.mts +2 -1
- package/dist/init/index.mjs +51 -25
- package/package.json +6 -6
package/dist/functions/index.mjs
CHANGED
|
@@ -8118,7 +8118,7 @@ const createKvAdapter = ({ store, tablePrefix = "" }) => {
|
|
|
8118
8118
|
};
|
|
8119
8119
|
//#endregion
|
|
8120
8120
|
//#region ../../packages/server/dist/version.mjs
|
|
8121
|
-
const HOT_UPDATER_SERVER_VERSION = "1.2.0-rc.
|
|
8121
|
+
const HOT_UPDATER_SERVER_VERSION = "1.2.0-rc.6";
|
|
8122
8122
|
//#endregion
|
|
8123
8123
|
//#region ../../packages/server/dist/handlerVersionRoutes.mjs
|
|
8124
8124
|
const createVersionRouteHandlers = (plugins = []) => {
|
|
@@ -57415,12 +57415,19 @@ const PROTOCOL = "azblob";
|
|
|
57415
57415
|
const SAS_EXPIRES_IN_MS = 36e5;
|
|
57416
57416
|
const DELEGATION_KEY_LIFETIME_MS = 864e5;
|
|
57417
57417
|
const isNotFound = (error) => error instanceof RestError && error.statusCode === 404;
|
|
57418
|
-
const serviceOf = (config) =>
|
|
57418
|
+
const serviceOf = async (config) => {
|
|
57419
|
+
if (!("connectionString" in config)) return new BlobServiceClient(config.accountUrl, config.credential);
|
|
57420
|
+
const { connectionString } = config;
|
|
57421
|
+
return BlobServiceClient.fromConnectionString(typeof connectionString === "function" ? await connectionString() : connectionString);
|
|
57422
|
+
};
|
|
57419
57423
|
function azureBlobStorage(config) {
|
|
57420
57424
|
const { containerName, basePath, getDownloadUrl } = config;
|
|
57421
57425
|
let client;
|
|
57422
|
-
const service = () => client ??= serviceOf(config)
|
|
57423
|
-
|
|
57426
|
+
const service = () => client ??= serviceOf(config).catch((error) => {
|
|
57427
|
+
client = void 0;
|
|
57428
|
+
throw error;
|
|
57429
|
+
});
|
|
57430
|
+
const container = async () => (await service()).getContainerClient(containerName);
|
|
57424
57431
|
const normalizedBasePath = basePath?.replace(/^\/+|\/+$/g, "") ?? "";
|
|
57425
57432
|
const getStorageKey = createStorageKeyBuilder(normalizedBasePath);
|
|
57426
57433
|
const getListPrefix = (prefix = "") => {
|
|
@@ -57451,7 +57458,7 @@ function azureBlobStorage(config) {
|
|
|
57451
57458
|
const delegationKey = () => {
|
|
57452
57459
|
const now = Date.now();
|
|
57453
57460
|
if (delegation === void 0 || delegation.renewAt <= now) {
|
|
57454
|
-
const key = service().getUserDelegationKey(/* @__PURE__ */ new Date(now - 3e5), new Date(now + DELEGATION_KEY_LIFETIME_MS));
|
|
57461
|
+
const key = service().then((blobService) => blobService.getUserDelegationKey(/* @__PURE__ */ new Date(now - 3e5), new Date(now + DELEGATION_KEY_LIFETIME_MS)));
|
|
57455
57462
|
key.catch(() => {
|
|
57456
57463
|
delegation = void 0;
|
|
57457
57464
|
});
|
|
@@ -57463,7 +57470,7 @@ function azureBlobStorage(config) {
|
|
|
57463
57470
|
return delegation.key;
|
|
57464
57471
|
};
|
|
57465
57472
|
const signedUrl = async (key) => {
|
|
57466
|
-
const blob = container().getBlobClient(key);
|
|
57473
|
+
const blob = (await container()).getBlobClient(key);
|
|
57467
57474
|
const expiresOn = new Date(Date.now() + SAS_EXPIRES_IN_MS);
|
|
57468
57475
|
const permissions = BlobSASPermissions.parse("r");
|
|
57469
57476
|
if (blob.credential instanceof StorageSharedKeyCredential) return blob.generateSasUrl({
|
|
@@ -57484,7 +57491,8 @@ function azureBlobStorage(config) {
|
|
|
57484
57491
|
protocol: PROTOCOL,
|
|
57485
57492
|
async listObjects(prefix) {
|
|
57486
57493
|
const objects = [];
|
|
57487
|
-
|
|
57494
|
+
const blobs = await container();
|
|
57495
|
+
for await (const blob of blobs.listBlobsFlat({ prefix: getListPrefix(prefix) })) {
|
|
57488
57496
|
const storageUri = getListedStorageUri(blob.name);
|
|
57489
57497
|
if (storageUri === null) continue;
|
|
57490
57498
|
objects.push({
|
|
@@ -57501,7 +57509,8 @@ function azureBlobStorage(config) {
|
|
|
57501
57509
|
for (let offset = 0; offset < keys.length; offset += 256) {
|
|
57502
57510
|
const batch = keys.slice(offset, offset + 256);
|
|
57503
57511
|
if (batch.length === 0) continue;
|
|
57504
|
-
const
|
|
57512
|
+
const blobs = await container();
|
|
57513
|
+
const failed = (await blobs.getBlobBatchClient().deleteBlobs(batch.map((key) => blobs.getBlobClient(key)))).subResponses.filter(({ status }) => status >= 300 && status !== 404);
|
|
57505
57514
|
if (failed.length > 0) throw new Error(`Failed to delete ${failed.length} Azure blob(s).`);
|
|
57506
57515
|
}
|
|
57507
57516
|
},
|
|
@@ -57511,7 +57520,7 @@ function azureBlobStorage(config) {
|
|
|
57511
57520
|
blobContentType: contentType,
|
|
57512
57521
|
blobCacheControl: "max-age=31536000"
|
|
57513
57522
|
} };
|
|
57514
|
-
const blob = container().getBlockBlobClient(storageKey);
|
|
57523
|
+
const blob = (await container()).getBlockBlobClient(storageKey);
|
|
57515
57524
|
if (contentLength === void 0) await blob.uploadData(new Uint8Array(await new Response(body).arrayBuffer()), options);
|
|
57516
57525
|
else {
|
|
57517
57526
|
const { Readable } = await import("node:stream");
|
|
@@ -57540,7 +57549,7 @@ function azureBlobStorage(config) {
|
|
|
57540
57549
|
async get({ storageUri }) {
|
|
57541
57550
|
const { key } = parseAndValidate(storageUri);
|
|
57542
57551
|
try {
|
|
57543
|
-
const download = await container().getBlobClient(key).download();
|
|
57552
|
+
const download = await (await container()).getBlobClient(key).download();
|
|
57544
57553
|
const body = download.readableStreamBody;
|
|
57545
57554
|
if (body === void 0) return { response: null };
|
|
57546
57555
|
const headers = new Headers();
|
|
@@ -57560,11 +57569,11 @@ function azureBlobStorage(config) {
|
|
|
57560
57569
|
},
|
|
57561
57570
|
async exists({ storageUri }) {
|
|
57562
57571
|
const { key } = parseAndValidate(storageUri);
|
|
57563
|
-
return { exists: await container().getBlobClient(key).exists() };
|
|
57572
|
+
return { exists: await (await container()).getBlobClient(key).exists() };
|
|
57564
57573
|
},
|
|
57565
57574
|
async delete({ storageUri }) {
|
|
57566
57575
|
const { key } = parseAndValidate(storageUri);
|
|
57567
|
-
await container().getBlobClient(key).deleteIfExists();
|
|
57576
|
+
await (await container()).getBlobClient(key).deleteIfExists();
|
|
57568
57577
|
return { deleted: true };
|
|
57569
57578
|
}
|
|
57570
57579
|
});
|
|
@@ -70857,18 +70866,21 @@ const isWideAdd = (op) => op.type === "add" && Object.keys(op.by).length + (op.e
|
|
|
70857
70866
|
const emulatorAborted = (error) => statusOf(error) === 500 && /PostgresError\(SqlState\(E(25P02|40001|40P01)\)/.test(String(error instanceof Error ? error.message : error));
|
|
70858
70867
|
const createCosmosStore = ({ client, databaseId, containerId, engine, nativePageSize }) => {
|
|
70859
70868
|
let opened;
|
|
70860
|
-
const open = () => opened ??= (() => {
|
|
70861
|
-
const account = typeof client === "function" ? client() : client;
|
|
70869
|
+
const open = () => opened ??= (async () => {
|
|
70870
|
+
const account = typeof client === "function" ? await client() : client;
|
|
70862
70871
|
return {
|
|
70863
70872
|
client: account,
|
|
70864
70873
|
container: account.database(databaseId).container(containerId)
|
|
70865
70874
|
};
|
|
70866
|
-
})()
|
|
70867
|
-
|
|
70875
|
+
})().catch((error) => {
|
|
70876
|
+
opened = void 0;
|
|
70877
|
+
throw error;
|
|
70878
|
+
});
|
|
70879
|
+
const container = async () => (await open()).container;
|
|
70868
70880
|
const readStates = async (ids, rowIds) => {
|
|
70869
70881
|
const states = /* @__PURE__ */ new Map();
|
|
70870
70882
|
for (const [group, projection] of [[ids.filter((id) => !rowIds.has(id)), ""], [ids.filter((id) => rowIds.has(id)), ", c.row"]]) for (let at = 0; at < group.length; at += GET_BATCH) {
|
|
70871
|
-
const { resources } = await container().items.query(`SELECT c.id, c._etag AS etag, c.row._v AS v, c.expireAt${projection} FROM c WHERE c.id IN (${idList(group.slice(at, at + GET_BATCH))})`, { partitionKey: COSMOS_PARTITION }).fetchAll().catch(missingContainer);
|
|
70883
|
+
const { resources } = await (await container()).items.query(`SELECT c.id, c._etag AS etag, c.row._v AS v, c.expireAt${projection} FROM c WHERE c.id IN (${idList(group.slice(at, at + GET_BATCH))})`, { partitionKey: COSMOS_PARTITION }).fetchAll().catch(missingContainer);
|
|
70872
70884
|
for (const { id, etag, v, expireAt, row } of resources) states.set(id, {
|
|
70873
70885
|
etag,
|
|
70874
70886
|
v,
|
|
@@ -71000,7 +71012,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
|
|
|
71000
71012
|
const found = /* @__PURE__ */ new Map();
|
|
71001
71013
|
const ids = [...new Set(keys.map(cosmosItemId))];
|
|
71002
71014
|
for (let at = 0; at < ids.length; at += GET_BATCH) {
|
|
71003
|
-
const { resources } = await container().items.query(`SELECT c.id, c.row FROM c WHERE c.id IN (${idList(ids.slice(at, at + GET_BATCH))})`, { partitionKey: COSMOS_PARTITION }).fetchAll().catch(missingContainer);
|
|
71015
|
+
const { resources } = await (await container()).items.query(`SELECT c.id, c.row FROM c WHERE c.id IN (${idList(ids.slice(at, at + GET_BATCH))})`, { partitionKey: COSMOS_PARTITION }).fetchAll().catch(missingContainer);
|
|
71004
71016
|
for (const resource of resources) found.set(resource.id, toRow(resource.row));
|
|
71005
71017
|
}
|
|
71006
71018
|
return keys.map((key) => found.get(cosmosItemId(key)) ?? null);
|
|
@@ -71036,7 +71048,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
|
|
|
71036
71048
|
value: after
|
|
71037
71049
|
});
|
|
71038
71050
|
}
|
|
71039
|
-
const { resources } = await container().items.query({
|
|
71051
|
+
const { resources } = await (await container()).items.query({
|
|
71040
71052
|
query: `SELECT TOP @size c.sk, c.row FROM c WHERE ${filters.join(" AND ")} ORDER BY c.pk ${order === "asc" ? "ASC" : "DESC"}, c.sk ${order === "asc" ? "ASC" : "DESC"}`,
|
|
71041
71053
|
parameters
|
|
71042
71054
|
}, {
|
|
@@ -71086,7 +71098,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
|
|
|
71086
71098
|
if (planned.length > BATCH_OPERATIONS) throw new Error(`A Cosmos DB batch takes ${BATCH_OPERATIONS} operations; this write needs ${planned.length}.`);
|
|
71087
71099
|
let results;
|
|
71088
71100
|
try {
|
|
71089
|
-
const response = await container().items.batch(planned.map(({ operation }) => operation), COSMOS_PARTITION);
|
|
71101
|
+
const response = await (await container()).items.batch(planned.map(({ operation }) => operation), COSMOS_PARTITION);
|
|
71090
71102
|
if (response.result === void 0) throw Object.assign(/* @__PURE__ */ new Error(`Cosmos DB refused the batch with status ${response.code}`), { code: response.code });
|
|
71091
71103
|
results = response.result;
|
|
71092
71104
|
} catch (error) {
|
|
@@ -71119,7 +71131,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
|
|
|
71119
71131
|
async deleteConsumed(keys) {
|
|
71120
71132
|
let remaining = [...new Set(keys.map(cosmosItemId))];
|
|
71121
71133
|
for (let attempt = 0; remaining.length > 0; attempt += 1) {
|
|
71122
|
-
const failed = (await container().items.executeBulkOperations(remaining.map((id) => ({
|
|
71134
|
+
const failed = (await (await container()).items.executeBulkOperations(remaining.map((id) => ({
|
|
71123
71135
|
operationType: "Delete",
|
|
71124
71136
|
id,
|
|
71125
71137
|
partitionKey: COSMOS_PARTITION
|
|
@@ -71138,8 +71150,8 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
|
|
|
71138
71150
|
}
|
|
71139
71151
|
},
|
|
71140
71152
|
migrations: { async apply() {
|
|
71141
|
-
if (engine !== void 0) await assertCosmosContainerReusable(container(), engine);
|
|
71142
|
-
const { database } = await open().client.databases.createIfNotExists({ id: databaseId });
|
|
71153
|
+
if (engine !== void 0) await assertCosmosContainerReusable(await container(), engine);
|
|
71154
|
+
const { database } = await (await open()).client.databases.createIfNotExists({ id: databaseId });
|
|
71143
71155
|
const { resource } = await database.containers.createIfNotExists({
|
|
71144
71156
|
id: containerId,
|
|
71145
71157
|
partitionKey: { paths: [COSMOS_PARTITION_KEY_PATH] },
|
|
@@ -71176,8 +71188,12 @@ const adapterOf = (config) => {
|
|
|
71176
71188
|
const { databaseId = AZURE_COSMOS_V1_DATABASE, containerId = AZURE_COSMOS_V1_CONTAINER, aggregateBatching: _, ...connection } = config;
|
|
71177
71189
|
if (connection.consistencyLevel !== void 0 && connection.consistencyLevel !== "Strong") throw new Error(`cosmosDatabase reads with strong consistency; consistencyLevel "${String(connection.consistencyLevel)}" is weaker.`);
|
|
71178
71190
|
let client;
|
|
71191
|
+
const { connectionString } = connection;
|
|
71179
71192
|
const store = createCosmosStore({
|
|
71180
|
-
client: () => client ??= new CosmosClient(
|
|
71193
|
+
client: async () => client ??= new CosmosClient({
|
|
71194
|
+
...connection,
|
|
71195
|
+
connectionString: typeof connectionString === "function" ? await connectionString() : connectionString
|
|
71196
|
+
}),
|
|
71181
71197
|
databaseId,
|
|
71182
71198
|
containerId,
|
|
71183
71199
|
engine: currentCosmosEngine()
|
|
@@ -71189,7 +71205,7 @@ const adapterOf = (config) => {
|
|
|
71189
71205
|
};
|
|
71190
71206
|
const DATABASE_TARGET = Symbol.for("@hot-updater/database-target");
|
|
71191
71207
|
const accountHostOf = ({ endpoint, connectionString }) => {
|
|
71192
|
-
const url = endpoint ?? connectionString
|
|
71208
|
+
const url = endpoint ?? (typeof connectionString === "string" ? connectionString.match(/AccountEndpoint=([^;]+)/iu)?.[1] : void 0);
|
|
71193
71209
|
try {
|
|
71194
71210
|
return url === void 0 ? void 0 : new URL(url).hostname;
|
|
71195
71211
|
} catch {
|
package/dist/index.cjs
CHANGED
|
@@ -11,7 +11,11 @@ const SAS_EXPIRES_IN_MS = 36e5;
|
|
|
11
11
|
/** A user delegation key signs SAS URLs for up to seven days; it is renewed well before. */
|
|
12
12
|
const DELEGATION_KEY_LIFETIME_MS = 864e5;
|
|
13
13
|
const isNotFound = (error) => error instanceof _azure_storage_blob.RestError && error.statusCode === 404;
|
|
14
|
-
const serviceOf = (config) =>
|
|
14
|
+
const serviceOf = async (config) => {
|
|
15
|
+
if (!("connectionString" in config)) return new _azure_storage_blob.BlobServiceClient(config.accountUrl, config.credential);
|
|
16
|
+
const { connectionString } = config;
|
|
17
|
+
return _azure_storage_blob.BlobServiceClient.fromConnectionString(typeof connectionString === "function" ? await connectionString() : connectionString);
|
|
18
|
+
};
|
|
15
19
|
/**
|
|
16
20
|
* Bundles in an Azure Blob Storage container. Without `getDownloadUrl`,
|
|
17
21
|
* devices download through SAS URLs: signed with the account key when the
|
|
@@ -21,8 +25,11 @@ const serviceOf = (config) => "connectionString" in config ? _azure_storage_blob
|
|
|
21
25
|
function azureBlobStorage(config) {
|
|
22
26
|
const { containerName, basePath, getDownloadUrl } = config;
|
|
23
27
|
let client;
|
|
24
|
-
const service = () => client ??= serviceOf(config)
|
|
25
|
-
|
|
28
|
+
const service = () => client ??= serviceOf(config).catch((error) => {
|
|
29
|
+
client = void 0;
|
|
30
|
+
throw error;
|
|
31
|
+
});
|
|
32
|
+
const container = async () => (await service()).getContainerClient(containerName);
|
|
26
33
|
const normalizedBasePath = basePath?.replace(/^\/+|\/+$/g, "") ?? "";
|
|
27
34
|
const getStorageKey = (0, _hot_updater_plugin_core.createStorageKeyBuilder)(normalizedBasePath);
|
|
28
35
|
const getListPrefix = (prefix = "") => {
|
|
@@ -54,7 +61,7 @@ function azureBlobStorage(config) {
|
|
|
54
61
|
const delegationKey = () => {
|
|
55
62
|
const now = Date.now();
|
|
56
63
|
if (delegation === void 0 || delegation.renewAt <= now) {
|
|
57
|
-
const key = service().getUserDelegationKey(/* @__PURE__ */ new Date(now - 3e5), new Date(now + DELEGATION_KEY_LIFETIME_MS));
|
|
64
|
+
const key = service().then((blobService) => blobService.getUserDelegationKey(/* @__PURE__ */ new Date(now - 3e5), new Date(now + DELEGATION_KEY_LIFETIME_MS)));
|
|
58
65
|
key.catch(() => {
|
|
59
66
|
delegation = void 0;
|
|
60
67
|
});
|
|
@@ -66,7 +73,7 @@ function azureBlobStorage(config) {
|
|
|
66
73
|
return delegation.key;
|
|
67
74
|
};
|
|
68
75
|
const signedUrl = async (key) => {
|
|
69
|
-
const blob = container().getBlobClient(key);
|
|
76
|
+
const blob = (await container()).getBlobClient(key);
|
|
70
77
|
const expiresOn = new Date(Date.now() + SAS_EXPIRES_IN_MS);
|
|
71
78
|
const permissions = _azure_storage_blob.BlobSASPermissions.parse("r");
|
|
72
79
|
if (blob.credential instanceof _azure_storage_blob.StorageSharedKeyCredential) return blob.generateSasUrl({
|
|
@@ -87,7 +94,8 @@ function azureBlobStorage(config) {
|
|
|
87
94
|
protocol: PROTOCOL,
|
|
88
95
|
async listObjects(prefix) {
|
|
89
96
|
const objects = [];
|
|
90
|
-
|
|
97
|
+
const blobs = await container();
|
|
98
|
+
for await (const blob of blobs.listBlobsFlat({ prefix: getListPrefix(prefix) })) {
|
|
91
99
|
const storageUri = getListedStorageUri(blob.name);
|
|
92
100
|
if (storageUri === null) continue;
|
|
93
101
|
objects.push({
|
|
@@ -104,7 +112,8 @@ function azureBlobStorage(config) {
|
|
|
104
112
|
for (let offset = 0; offset < keys.length; offset += 256) {
|
|
105
113
|
const batch = keys.slice(offset, offset + 256);
|
|
106
114
|
if (batch.length === 0) continue;
|
|
107
|
-
const
|
|
115
|
+
const blobs = await container();
|
|
116
|
+
const failed = (await blobs.getBlobBatchClient().deleteBlobs(batch.map((key) => blobs.getBlobClient(key)))).subResponses.filter(({ status }) => status >= 300 && status !== 404);
|
|
108
117
|
if (failed.length > 0) throw new Error(`Failed to delete ${failed.length} Azure blob(s).`);
|
|
109
118
|
}
|
|
110
119
|
},
|
|
@@ -114,7 +123,7 @@ function azureBlobStorage(config) {
|
|
|
114
123
|
blobContentType: contentType,
|
|
115
124
|
blobCacheControl: "max-age=31536000"
|
|
116
125
|
} };
|
|
117
|
-
const blob = container().getBlockBlobClient(storageKey);
|
|
126
|
+
const blob = (await container()).getBlockBlobClient(storageKey);
|
|
118
127
|
if (contentLength === void 0) await blob.uploadData(new Uint8Array(await new Response(body).arrayBuffer()), options);
|
|
119
128
|
else {
|
|
120
129
|
const { Readable } = await import("node:stream");
|
|
@@ -143,7 +152,7 @@ function azureBlobStorage(config) {
|
|
|
143
152
|
async get({ storageUri }) {
|
|
144
153
|
const { key } = parseAndValidate(storageUri);
|
|
145
154
|
try {
|
|
146
|
-
const download = await container().getBlobClient(key).download();
|
|
155
|
+
const download = await (await container()).getBlobClient(key).download();
|
|
147
156
|
const body = download.readableStreamBody;
|
|
148
157
|
if (body === void 0) return { response: null };
|
|
149
158
|
const headers = new Headers();
|
|
@@ -163,11 +172,11 @@ function azureBlobStorage(config) {
|
|
|
163
172
|
},
|
|
164
173
|
async exists({ storageUri }) {
|
|
165
174
|
const { key } = parseAndValidate(storageUri);
|
|
166
|
-
return { exists: await container().getBlobClient(key).exists() };
|
|
175
|
+
return { exists: await (await container()).getBlobClient(key).exists() };
|
|
167
176
|
},
|
|
168
177
|
async delete({ storageUri }) {
|
|
169
178
|
const { key } = parseAndValidate(storageUri);
|
|
170
|
-
await container().getBlobClient(key).deleteIfExists();
|
|
179
|
+
await (await container()).getBlobClient(key).deleteIfExists();
|
|
171
180
|
return { deleted: true };
|
|
172
181
|
}
|
|
173
182
|
});
|
|
@@ -385,14 +394,17 @@ const emulatorAborted = (error) => statusOf(error) === 500 && /PostgresError\(Sq
|
|
|
385
394
|
*/
|
|
386
395
|
const createCosmosStore = ({ client, databaseId, containerId, engine, nativePageSize }) => {
|
|
387
396
|
let opened;
|
|
388
|
-
const open = () => opened ??= (() => {
|
|
389
|
-
const account = typeof client === "function" ? client() : client;
|
|
397
|
+
const open = () => opened ??= (async () => {
|
|
398
|
+
const account = typeof client === "function" ? await client() : client;
|
|
390
399
|
return {
|
|
391
400
|
client: account,
|
|
392
401
|
container: account.database(databaseId).container(containerId)
|
|
393
402
|
};
|
|
394
|
-
})()
|
|
395
|
-
|
|
403
|
+
})().catch((error) => {
|
|
404
|
+
opened = void 0;
|
|
405
|
+
throw error;
|
|
406
|
+
});
|
|
407
|
+
const container = async () => (await open()).container;
|
|
396
408
|
/**
|
|
397
409
|
* The state of the items `ids` names, by id, with the rows of `rowIds`;
|
|
398
410
|
* a missing item has none.
|
|
@@ -400,7 +412,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
|
|
|
400
412
|
const readStates = async (ids, rowIds) => {
|
|
401
413
|
const states = /* @__PURE__ */ new Map();
|
|
402
414
|
for (const [group, projection] of [[ids.filter((id) => !rowIds.has(id)), ""], [ids.filter((id) => rowIds.has(id)), ", c.row"]]) for (let at = 0; at < group.length; at += GET_BATCH) {
|
|
403
|
-
const { resources } = await container().items.query(`SELECT c.id, c._etag AS etag, c.row._v AS v, c.expireAt${projection} FROM c WHERE c.id IN (${idList(group.slice(at, at + GET_BATCH))})`, { partitionKey: COSMOS_PARTITION }).fetchAll().catch(missingContainer);
|
|
415
|
+
const { resources } = await (await container()).items.query(`SELECT c.id, c._etag AS etag, c.row._v AS v, c.expireAt${projection} FROM c WHERE c.id IN (${idList(group.slice(at, at + GET_BATCH))})`, { partitionKey: COSMOS_PARTITION }).fetchAll().catch(missingContainer);
|
|
404
416
|
for (const { id, etag, v, expireAt, row } of resources) states.set(id, {
|
|
405
417
|
etag,
|
|
406
418
|
v,
|
|
@@ -538,7 +550,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
|
|
|
538
550
|
const found = /* @__PURE__ */ new Map();
|
|
539
551
|
const ids = [...new Set(keys.map(cosmosItemId))];
|
|
540
552
|
for (let at = 0; at < ids.length; at += GET_BATCH) {
|
|
541
|
-
const { resources } = await container().items.query(`SELECT c.id, c.row FROM c WHERE c.id IN (${idList(ids.slice(at, at + GET_BATCH))})`, { partitionKey: COSMOS_PARTITION }).fetchAll().catch(missingContainer);
|
|
553
|
+
const { resources } = await (await container()).items.query(`SELECT c.id, c.row FROM c WHERE c.id IN (${idList(ids.slice(at, at + GET_BATCH))})`, { partitionKey: COSMOS_PARTITION }).fetchAll().catch(missingContainer);
|
|
542
554
|
for (const resource of resources) found.set(resource.id, toRow(resource.row));
|
|
543
555
|
}
|
|
544
556
|
return keys.map((key) => found.get(cosmosItemId(key)) ?? null);
|
|
@@ -574,7 +586,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
|
|
|
574
586
|
value: after
|
|
575
587
|
});
|
|
576
588
|
}
|
|
577
|
-
const { resources } = await container().items.query({
|
|
589
|
+
const { resources } = await (await container()).items.query({
|
|
578
590
|
query: `SELECT TOP @size c.sk, c.row FROM c WHERE ${filters.join(" AND ")} ORDER BY c.pk ${order === "asc" ? "ASC" : "DESC"}, c.sk ${order === "asc" ? "ASC" : "DESC"}`,
|
|
579
591
|
parameters
|
|
580
592
|
}, {
|
|
@@ -624,7 +636,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
|
|
|
624
636
|
if (planned.length > BATCH_OPERATIONS) throw new Error(`A Cosmos DB batch takes ${BATCH_OPERATIONS} operations; this write needs ${planned.length}.`);
|
|
625
637
|
let results;
|
|
626
638
|
try {
|
|
627
|
-
const response = await container().items.batch(planned.map(({ operation }) => operation), COSMOS_PARTITION);
|
|
639
|
+
const response = await (await container()).items.batch(planned.map(({ operation }) => operation), COSMOS_PARTITION);
|
|
628
640
|
if (response.result === void 0) throw Object.assign(/* @__PURE__ */ new Error(`Cosmos DB refused the batch with status ${response.code}`), { code: response.code });
|
|
629
641
|
results = response.result;
|
|
630
642
|
} catch (error) {
|
|
@@ -658,7 +670,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
|
|
|
658
670
|
async deleteConsumed(keys) {
|
|
659
671
|
let remaining = [...new Set(keys.map(cosmosItemId))];
|
|
660
672
|
for (let attempt = 0; remaining.length > 0; attempt += 1) {
|
|
661
|
-
const failed = (await container().items.executeBulkOperations(remaining.map((id) => ({
|
|
673
|
+
const failed = (await (await container()).items.executeBulkOperations(remaining.map((id) => ({
|
|
662
674
|
operationType: "Delete",
|
|
663
675
|
id,
|
|
664
676
|
partitionKey: COSMOS_PARTITION
|
|
@@ -679,8 +691,8 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
|
|
|
679
691
|
migrations: {
|
|
680
692
|
/** Creates the database and the container when they are missing. */
|
|
681
693
|
async apply() {
|
|
682
|
-
if (engine !== void 0) await assertCosmosContainerReusable(container(), engine);
|
|
683
|
-
const { database } = await open().client.databases.createIfNotExists({ id: databaseId });
|
|
694
|
+
if (engine !== void 0) await assertCosmosContainerReusable(await container(), engine);
|
|
695
|
+
const { database } = await (await open()).client.databases.createIfNotExists({ id: databaseId });
|
|
684
696
|
const { resource } = await database.containers.createIfNotExists({
|
|
685
697
|
id: containerId,
|
|
686
698
|
partitionKey: { paths: [COSMOS_PARTITION_KEY_PATH] },
|
|
@@ -717,8 +729,12 @@ const adapterOf = (config) => {
|
|
|
717
729
|
const { databaseId = AZURE_COSMOS_V1_DATABASE, containerId = AZURE_COSMOS_V1_CONTAINER, aggregateBatching: _, ...connection } = config;
|
|
718
730
|
if (connection.consistencyLevel !== void 0 && connection.consistencyLevel !== "Strong") throw new Error(`cosmosDatabase reads with strong consistency; consistencyLevel "${String(connection.consistencyLevel)}" is weaker.`);
|
|
719
731
|
let client;
|
|
732
|
+
const { connectionString } = connection;
|
|
720
733
|
const store = createCosmosStore({
|
|
721
|
-
client: () => client ??= new _azure_cosmos.CosmosClient(
|
|
734
|
+
client: async () => client ??= new _azure_cosmos.CosmosClient({
|
|
735
|
+
...connection,
|
|
736
|
+
connectionString: typeof connectionString === "function" ? await connectionString() : connectionString
|
|
737
|
+
}),
|
|
722
738
|
databaseId,
|
|
723
739
|
containerId,
|
|
724
740
|
engine: currentCosmosEngine()
|
|
@@ -747,7 +763,7 @@ const migrateCosmosDatabase = (config, plugins = []) => {
|
|
|
747
763
|
const DATABASE_TARGET = Symbol.for("@hot-updater/database-target");
|
|
748
764
|
/** The account host a client reaches: its endpoint, or the connection string's. */
|
|
749
765
|
const accountHostOf = ({ endpoint, connectionString }) => {
|
|
750
|
-
const url = endpoint ?? connectionString
|
|
766
|
+
const url = endpoint ?? (typeof connectionString === "string" ? connectionString.match(/AccountEndpoint=([^;]+)/iu)?.[1] : void 0);
|
|
751
767
|
try {
|
|
752
768
|
return url === void 0 ? void 0 : new URL(url).hostname;
|
|
753
769
|
} catch {
|
package/dist/index.d.cts
CHANGED
|
@@ -5,7 +5,12 @@ import { CosmosClientOptions } from "@azure/cosmos";
|
|
|
5
5
|
//#region src/azureBlobStorage.d.ts
|
|
6
6
|
/** The account to connect to: its connection string, or its Blob endpoint and a credential. */
|
|
7
7
|
type AzureBlobConnection = {
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* The account's connection string, or a function that returns it,
|
|
10
|
+
* such as one that reads it with `secretEnv`: called on first use, so
|
|
11
|
+
* a config loads without it.
|
|
12
|
+
*/
|
|
13
|
+
readonly connectionString: string | (() => string | Promise<string>);
|
|
9
14
|
} | {
|
|
10
15
|
/** The Blob service endpoint, such as `https://<account>.blob.core.windows.net`. */
|
|
11
16
|
readonly accountUrl: string;
|
|
@@ -38,7 +43,14 @@ declare function azureBlobStorage(config: AzureBlobStorageConfig): StorageAdapte
|
|
|
38
43
|
* `endpoint` with a `key` or Microsoft Entra `aadCredentials`. Reads must
|
|
39
44
|
* be strongly consistent, so `consistencyLevel` takes only `"Strong"`.
|
|
40
45
|
*/
|
|
41
|
-
type CosmosDatabaseConfig = Omit<CosmosClientOptions, "consistencyLevel"> & {
|
|
46
|
+
type CosmosDatabaseConfig = Omit<CosmosClientOptions, "consistencyLevel" | "connectionString"> & {
|
|
47
|
+
/**
|
|
48
|
+
* The account's connection string, or a function that returns it, such as
|
|
49
|
+
* one that reads it with `secretEnv`: called on the first request, so a
|
|
50
|
+
* config loads without it. `endpoint` beside a function names the
|
|
51
|
+
* account to `hot-updater doctor`.
|
|
52
|
+
*/
|
|
53
|
+
readonly connectionString?: string | (() => string | Promise<string>);
|
|
42
54
|
readonly consistencyLevel?: "Strong";
|
|
43
55
|
/** The database the container lives in. @default "hot-updater" */
|
|
44
56
|
readonly databaseId?: string;
|
package/dist/index.d.mts
CHANGED
|
@@ -5,7 +5,12 @@ import { CosmosClientOptions } from "@azure/cosmos";
|
|
|
5
5
|
//#region src/azureBlobStorage.d.ts
|
|
6
6
|
/** The account to connect to: its connection string, or its Blob endpoint and a credential. */
|
|
7
7
|
type AzureBlobConnection = {
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* The account's connection string, or a function that returns it,
|
|
10
|
+
* such as one that reads it with `secretEnv`: called on first use, so
|
|
11
|
+
* a config loads without it.
|
|
12
|
+
*/
|
|
13
|
+
readonly connectionString: string | (() => string | Promise<string>);
|
|
9
14
|
} | {
|
|
10
15
|
/** The Blob service endpoint, such as `https://<account>.blob.core.windows.net`. */
|
|
11
16
|
readonly accountUrl: string;
|
|
@@ -38,7 +43,14 @@ declare function azureBlobStorage(config: AzureBlobStorageConfig): StorageAdapte
|
|
|
38
43
|
* `endpoint` with a `key` or Microsoft Entra `aadCredentials`. Reads must
|
|
39
44
|
* be strongly consistent, so `consistencyLevel` takes only `"Strong"`.
|
|
40
45
|
*/
|
|
41
|
-
type CosmosDatabaseConfig = Omit<CosmosClientOptions, "consistencyLevel"> & {
|
|
46
|
+
type CosmosDatabaseConfig = Omit<CosmosClientOptions, "consistencyLevel" | "connectionString"> & {
|
|
47
|
+
/**
|
|
48
|
+
* The account's connection string, or a function that returns it, such as
|
|
49
|
+
* one that reads it with `secretEnv`: called on the first request, so a
|
|
50
|
+
* config loads without it. `endpoint` beside a function names the
|
|
51
|
+
* account to `hot-updater doctor`.
|
|
52
|
+
*/
|
|
53
|
+
readonly connectionString?: string | (() => string | Promise<string>);
|
|
42
54
|
readonly consistencyLevel?: "Strong";
|
|
43
55
|
/** The database the container lives in. @default "hot-updater" */
|
|
44
56
|
readonly databaseId?: string;
|
package/dist/index.mjs
CHANGED
|
@@ -10,7 +10,11 @@ const SAS_EXPIRES_IN_MS = 36e5;
|
|
|
10
10
|
/** A user delegation key signs SAS URLs for up to seven days; it is renewed well before. */
|
|
11
11
|
const DELEGATION_KEY_LIFETIME_MS = 864e5;
|
|
12
12
|
const isNotFound = (error) => error instanceof RestError && error.statusCode === 404;
|
|
13
|
-
const serviceOf = (config) =>
|
|
13
|
+
const serviceOf = async (config) => {
|
|
14
|
+
if (!("connectionString" in config)) return new BlobServiceClient(config.accountUrl, config.credential);
|
|
15
|
+
const { connectionString } = config;
|
|
16
|
+
return BlobServiceClient.fromConnectionString(typeof connectionString === "function" ? await connectionString() : connectionString);
|
|
17
|
+
};
|
|
14
18
|
/**
|
|
15
19
|
* Bundles in an Azure Blob Storage container. Without `getDownloadUrl`,
|
|
16
20
|
* devices download through SAS URLs: signed with the account key when the
|
|
@@ -20,8 +24,11 @@ const serviceOf = (config) => "connectionString" in config ? BlobServiceClient.f
|
|
|
20
24
|
function azureBlobStorage(config) {
|
|
21
25
|
const { containerName, basePath, getDownloadUrl } = config;
|
|
22
26
|
let client;
|
|
23
|
-
const service = () => client ??= serviceOf(config)
|
|
24
|
-
|
|
27
|
+
const service = () => client ??= serviceOf(config).catch((error) => {
|
|
28
|
+
client = void 0;
|
|
29
|
+
throw error;
|
|
30
|
+
});
|
|
31
|
+
const container = async () => (await service()).getContainerClient(containerName);
|
|
25
32
|
const normalizedBasePath = basePath?.replace(/^\/+|\/+$/g, "") ?? "";
|
|
26
33
|
const getStorageKey = createStorageKeyBuilder(normalizedBasePath);
|
|
27
34
|
const getListPrefix = (prefix = "") => {
|
|
@@ -53,7 +60,7 @@ function azureBlobStorage(config) {
|
|
|
53
60
|
const delegationKey = () => {
|
|
54
61
|
const now = Date.now();
|
|
55
62
|
if (delegation === void 0 || delegation.renewAt <= now) {
|
|
56
|
-
const key = service().getUserDelegationKey(/* @__PURE__ */ new Date(now - 3e5), new Date(now + DELEGATION_KEY_LIFETIME_MS));
|
|
63
|
+
const key = service().then((blobService) => blobService.getUserDelegationKey(/* @__PURE__ */ new Date(now - 3e5), new Date(now + DELEGATION_KEY_LIFETIME_MS)));
|
|
57
64
|
key.catch(() => {
|
|
58
65
|
delegation = void 0;
|
|
59
66
|
});
|
|
@@ -65,7 +72,7 @@ function azureBlobStorage(config) {
|
|
|
65
72
|
return delegation.key;
|
|
66
73
|
};
|
|
67
74
|
const signedUrl = async (key) => {
|
|
68
|
-
const blob = container().getBlobClient(key);
|
|
75
|
+
const blob = (await container()).getBlobClient(key);
|
|
69
76
|
const expiresOn = new Date(Date.now() + SAS_EXPIRES_IN_MS);
|
|
70
77
|
const permissions = BlobSASPermissions.parse("r");
|
|
71
78
|
if (blob.credential instanceof StorageSharedKeyCredential) return blob.generateSasUrl({
|
|
@@ -86,7 +93,8 @@ function azureBlobStorage(config) {
|
|
|
86
93
|
protocol: PROTOCOL,
|
|
87
94
|
async listObjects(prefix) {
|
|
88
95
|
const objects = [];
|
|
89
|
-
|
|
96
|
+
const blobs = await container();
|
|
97
|
+
for await (const blob of blobs.listBlobsFlat({ prefix: getListPrefix(prefix) })) {
|
|
90
98
|
const storageUri = getListedStorageUri(blob.name);
|
|
91
99
|
if (storageUri === null) continue;
|
|
92
100
|
objects.push({
|
|
@@ -103,7 +111,8 @@ function azureBlobStorage(config) {
|
|
|
103
111
|
for (let offset = 0; offset < keys.length; offset += 256) {
|
|
104
112
|
const batch = keys.slice(offset, offset + 256);
|
|
105
113
|
if (batch.length === 0) continue;
|
|
106
|
-
const
|
|
114
|
+
const blobs = await container();
|
|
115
|
+
const failed = (await blobs.getBlobBatchClient().deleteBlobs(batch.map((key) => blobs.getBlobClient(key)))).subResponses.filter(({ status }) => status >= 300 && status !== 404);
|
|
107
116
|
if (failed.length > 0) throw new Error(`Failed to delete ${failed.length} Azure blob(s).`);
|
|
108
117
|
}
|
|
109
118
|
},
|
|
@@ -113,7 +122,7 @@ function azureBlobStorage(config) {
|
|
|
113
122
|
blobContentType: contentType,
|
|
114
123
|
blobCacheControl: "max-age=31536000"
|
|
115
124
|
} };
|
|
116
|
-
const blob = container().getBlockBlobClient(storageKey);
|
|
125
|
+
const blob = (await container()).getBlockBlobClient(storageKey);
|
|
117
126
|
if (contentLength === void 0) await blob.uploadData(new Uint8Array(await new Response(body).arrayBuffer()), options);
|
|
118
127
|
else {
|
|
119
128
|
const { Readable } = await import("node:stream");
|
|
@@ -142,7 +151,7 @@ function azureBlobStorage(config) {
|
|
|
142
151
|
async get({ storageUri }) {
|
|
143
152
|
const { key } = parseAndValidate(storageUri);
|
|
144
153
|
try {
|
|
145
|
-
const download = await container().getBlobClient(key).download();
|
|
154
|
+
const download = await (await container()).getBlobClient(key).download();
|
|
146
155
|
const body = download.readableStreamBody;
|
|
147
156
|
if (body === void 0) return { response: null };
|
|
148
157
|
const headers = new Headers();
|
|
@@ -162,11 +171,11 @@ function azureBlobStorage(config) {
|
|
|
162
171
|
},
|
|
163
172
|
async exists({ storageUri }) {
|
|
164
173
|
const { key } = parseAndValidate(storageUri);
|
|
165
|
-
return { exists: await container().getBlobClient(key).exists() };
|
|
174
|
+
return { exists: await (await container()).getBlobClient(key).exists() };
|
|
166
175
|
},
|
|
167
176
|
async delete({ storageUri }) {
|
|
168
177
|
const { key } = parseAndValidate(storageUri);
|
|
169
|
-
await container().getBlobClient(key).deleteIfExists();
|
|
178
|
+
await (await container()).getBlobClient(key).deleteIfExists();
|
|
170
179
|
return { deleted: true };
|
|
171
180
|
}
|
|
172
181
|
});
|
|
@@ -384,14 +393,17 @@ const emulatorAborted = (error) => statusOf(error) === 500 && /PostgresError\(Sq
|
|
|
384
393
|
*/
|
|
385
394
|
const createCosmosStore = ({ client, databaseId, containerId, engine, nativePageSize }) => {
|
|
386
395
|
let opened;
|
|
387
|
-
const open = () => opened ??= (() => {
|
|
388
|
-
const account = typeof client === "function" ? client() : client;
|
|
396
|
+
const open = () => opened ??= (async () => {
|
|
397
|
+
const account = typeof client === "function" ? await client() : client;
|
|
389
398
|
return {
|
|
390
399
|
client: account,
|
|
391
400
|
container: account.database(databaseId).container(containerId)
|
|
392
401
|
};
|
|
393
|
-
})()
|
|
394
|
-
|
|
402
|
+
})().catch((error) => {
|
|
403
|
+
opened = void 0;
|
|
404
|
+
throw error;
|
|
405
|
+
});
|
|
406
|
+
const container = async () => (await open()).container;
|
|
395
407
|
/**
|
|
396
408
|
* The state of the items `ids` names, by id, with the rows of `rowIds`;
|
|
397
409
|
* a missing item has none.
|
|
@@ -399,7 +411,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
|
|
|
399
411
|
const readStates = async (ids, rowIds) => {
|
|
400
412
|
const states = /* @__PURE__ */ new Map();
|
|
401
413
|
for (const [group, projection] of [[ids.filter((id) => !rowIds.has(id)), ""], [ids.filter((id) => rowIds.has(id)), ", c.row"]]) for (let at = 0; at < group.length; at += GET_BATCH) {
|
|
402
|
-
const { resources } = await container().items.query(`SELECT c.id, c._etag AS etag, c.row._v AS v, c.expireAt${projection} FROM c WHERE c.id IN (${idList(group.slice(at, at + GET_BATCH))})`, { partitionKey: COSMOS_PARTITION }).fetchAll().catch(missingContainer);
|
|
414
|
+
const { resources } = await (await container()).items.query(`SELECT c.id, c._etag AS etag, c.row._v AS v, c.expireAt${projection} FROM c WHERE c.id IN (${idList(group.slice(at, at + GET_BATCH))})`, { partitionKey: COSMOS_PARTITION }).fetchAll().catch(missingContainer);
|
|
403
415
|
for (const { id, etag, v, expireAt, row } of resources) states.set(id, {
|
|
404
416
|
etag,
|
|
405
417
|
v,
|
|
@@ -537,7 +549,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
|
|
|
537
549
|
const found = /* @__PURE__ */ new Map();
|
|
538
550
|
const ids = [...new Set(keys.map(cosmosItemId))];
|
|
539
551
|
for (let at = 0; at < ids.length; at += GET_BATCH) {
|
|
540
|
-
const { resources } = await container().items.query(`SELECT c.id, c.row FROM c WHERE c.id IN (${idList(ids.slice(at, at + GET_BATCH))})`, { partitionKey: COSMOS_PARTITION }).fetchAll().catch(missingContainer);
|
|
552
|
+
const { resources } = await (await container()).items.query(`SELECT c.id, c.row FROM c WHERE c.id IN (${idList(ids.slice(at, at + GET_BATCH))})`, { partitionKey: COSMOS_PARTITION }).fetchAll().catch(missingContainer);
|
|
541
553
|
for (const resource of resources) found.set(resource.id, toRow(resource.row));
|
|
542
554
|
}
|
|
543
555
|
return keys.map((key) => found.get(cosmosItemId(key)) ?? null);
|
|
@@ -573,7 +585,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
|
|
|
573
585
|
value: after
|
|
574
586
|
});
|
|
575
587
|
}
|
|
576
|
-
const { resources } = await container().items.query({
|
|
588
|
+
const { resources } = await (await container()).items.query({
|
|
577
589
|
query: `SELECT TOP @size c.sk, c.row FROM c WHERE ${filters.join(" AND ")} ORDER BY c.pk ${order === "asc" ? "ASC" : "DESC"}, c.sk ${order === "asc" ? "ASC" : "DESC"}`,
|
|
578
590
|
parameters
|
|
579
591
|
}, {
|
|
@@ -623,7 +635,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
|
|
|
623
635
|
if (planned.length > BATCH_OPERATIONS) throw new Error(`A Cosmos DB batch takes ${BATCH_OPERATIONS} operations; this write needs ${planned.length}.`);
|
|
624
636
|
let results;
|
|
625
637
|
try {
|
|
626
|
-
const response = await container().items.batch(planned.map(({ operation }) => operation), COSMOS_PARTITION);
|
|
638
|
+
const response = await (await container()).items.batch(planned.map(({ operation }) => operation), COSMOS_PARTITION);
|
|
627
639
|
if (response.result === void 0) throw Object.assign(/* @__PURE__ */ new Error(`Cosmos DB refused the batch with status ${response.code}`), { code: response.code });
|
|
628
640
|
results = response.result;
|
|
629
641
|
} catch (error) {
|
|
@@ -657,7 +669,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
|
|
|
657
669
|
async deleteConsumed(keys) {
|
|
658
670
|
let remaining = [...new Set(keys.map(cosmosItemId))];
|
|
659
671
|
for (let attempt = 0; remaining.length > 0; attempt += 1) {
|
|
660
|
-
const failed = (await container().items.executeBulkOperations(remaining.map((id) => ({
|
|
672
|
+
const failed = (await (await container()).items.executeBulkOperations(remaining.map((id) => ({
|
|
661
673
|
operationType: "Delete",
|
|
662
674
|
id,
|
|
663
675
|
partitionKey: COSMOS_PARTITION
|
|
@@ -678,8 +690,8 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
|
|
|
678
690
|
migrations: {
|
|
679
691
|
/** Creates the database and the container when they are missing. */
|
|
680
692
|
async apply() {
|
|
681
|
-
if (engine !== void 0) await assertCosmosContainerReusable(container(), engine);
|
|
682
|
-
const { database } = await open().client.databases.createIfNotExists({ id: databaseId });
|
|
693
|
+
if (engine !== void 0) await assertCosmosContainerReusable(await container(), engine);
|
|
694
|
+
const { database } = await (await open()).client.databases.createIfNotExists({ id: databaseId });
|
|
683
695
|
const { resource } = await database.containers.createIfNotExists({
|
|
684
696
|
id: containerId,
|
|
685
697
|
partitionKey: { paths: [COSMOS_PARTITION_KEY_PATH] },
|
|
@@ -716,8 +728,12 @@ const adapterOf = (config) => {
|
|
|
716
728
|
const { databaseId = AZURE_COSMOS_V1_DATABASE, containerId = AZURE_COSMOS_V1_CONTAINER, aggregateBatching: _, ...connection } = config;
|
|
717
729
|
if (connection.consistencyLevel !== void 0 && connection.consistencyLevel !== "Strong") throw new Error(`cosmosDatabase reads with strong consistency; consistencyLevel "${String(connection.consistencyLevel)}" is weaker.`);
|
|
718
730
|
let client;
|
|
731
|
+
const { connectionString } = connection;
|
|
719
732
|
const store = createCosmosStore({
|
|
720
|
-
client: () => client ??= new CosmosClient(
|
|
733
|
+
client: async () => client ??= new CosmosClient({
|
|
734
|
+
...connection,
|
|
735
|
+
connectionString: typeof connectionString === "function" ? await connectionString() : connectionString
|
|
736
|
+
}),
|
|
721
737
|
databaseId,
|
|
722
738
|
containerId,
|
|
723
739
|
engine: currentCosmosEngine()
|
|
@@ -746,7 +762,7 @@ const migrateCosmosDatabase = (config, plugins = []) => {
|
|
|
746
762
|
const DATABASE_TARGET = Symbol.for("@hot-updater/database-target");
|
|
747
763
|
/** The account host a client reaches: its endpoint, or the connection string's. */
|
|
748
764
|
const accountHostOf = ({ endpoint, connectionString }) => {
|
|
749
|
-
const url = endpoint ?? connectionString
|
|
765
|
+
const url = endpoint ?? (typeof connectionString === "string" ? connectionString.match(/AccountEndpoint=([^;]+)/iu)?.[1] : void 0);
|
|
750
766
|
try {
|
|
751
767
|
return url === void 0 ? void 0 : new URL(url).hostname;
|
|
752
768
|
} catch {
|
package/dist/init/index.cjs
CHANGED
|
@@ -267,14 +267,17 @@ const emulatorAborted = (error) => statusOf(error) === 500 && /PostgresError\(Sq
|
|
|
267
267
|
*/
|
|
268
268
|
const createCosmosStore = ({ client, databaseId, containerId, engine, nativePageSize }) => {
|
|
269
269
|
let opened;
|
|
270
|
-
const open = () => opened ??= (() => {
|
|
271
|
-
const account = typeof client === "function" ? client() : client;
|
|
270
|
+
const open = () => opened ??= (async () => {
|
|
271
|
+
const account = typeof client === "function" ? await client() : client;
|
|
272
272
|
return {
|
|
273
273
|
client: account,
|
|
274
274
|
container: account.database(databaseId).container(containerId)
|
|
275
275
|
};
|
|
276
|
-
})()
|
|
277
|
-
|
|
276
|
+
})().catch((error) => {
|
|
277
|
+
opened = void 0;
|
|
278
|
+
throw error;
|
|
279
|
+
});
|
|
280
|
+
const container = async () => (await open()).container;
|
|
278
281
|
/**
|
|
279
282
|
* The state of the items `ids` names, by id, with the rows of `rowIds`;
|
|
280
283
|
* a missing item has none.
|
|
@@ -282,7 +285,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
|
|
|
282
285
|
const readStates = async (ids, rowIds) => {
|
|
283
286
|
const states = /* @__PURE__ */ new Map();
|
|
284
287
|
for (const [group, projection] of [[ids.filter((id) => !rowIds.has(id)), ""], [ids.filter((id) => rowIds.has(id)), ", c.row"]]) for (let at = 0; at < group.length; at += GET_BATCH) {
|
|
285
|
-
const { resources } = await container().items.query(`SELECT c.id, c._etag AS etag, c.row._v AS v, c.expireAt${projection} FROM c WHERE c.id IN (${idList(group.slice(at, at + GET_BATCH))})`, { partitionKey: COSMOS_PARTITION }).fetchAll().catch(missingContainer);
|
|
288
|
+
const { resources } = await (await container()).items.query(`SELECT c.id, c._etag AS etag, c.row._v AS v, c.expireAt${projection} FROM c WHERE c.id IN (${idList(group.slice(at, at + GET_BATCH))})`, { partitionKey: COSMOS_PARTITION }).fetchAll().catch(missingContainer);
|
|
286
289
|
for (const { id, etag, v, expireAt, row } of resources) states.set(id, {
|
|
287
290
|
etag,
|
|
288
291
|
v,
|
|
@@ -420,7 +423,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
|
|
|
420
423
|
const found = /* @__PURE__ */ new Map();
|
|
421
424
|
const ids = [...new Set(keys.map(cosmosItemId))];
|
|
422
425
|
for (let at = 0; at < ids.length; at += GET_BATCH) {
|
|
423
|
-
const { resources } = await container().items.query(`SELECT c.id, c.row FROM c WHERE c.id IN (${idList(ids.slice(at, at + GET_BATCH))})`, { partitionKey: COSMOS_PARTITION }).fetchAll().catch(missingContainer);
|
|
426
|
+
const { resources } = await (await container()).items.query(`SELECT c.id, c.row FROM c WHERE c.id IN (${idList(ids.slice(at, at + GET_BATCH))})`, { partitionKey: COSMOS_PARTITION }).fetchAll().catch(missingContainer);
|
|
424
427
|
for (const resource of resources) found.set(resource.id, toRow(resource.row));
|
|
425
428
|
}
|
|
426
429
|
return keys.map((key) => found.get(cosmosItemId(key)) ?? null);
|
|
@@ -456,7 +459,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
|
|
|
456
459
|
value: after
|
|
457
460
|
});
|
|
458
461
|
}
|
|
459
|
-
const { resources } = await container().items.query({
|
|
462
|
+
const { resources } = await (await container()).items.query({
|
|
460
463
|
query: `SELECT TOP @size c.sk, c.row FROM c WHERE ${filters.join(" AND ")} ORDER BY c.pk ${order === "asc" ? "ASC" : "DESC"}, c.sk ${order === "asc" ? "ASC" : "DESC"}`,
|
|
461
464
|
parameters
|
|
462
465
|
}, {
|
|
@@ -506,7 +509,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
|
|
|
506
509
|
if (planned.length > BATCH_OPERATIONS) throw new Error(`A Cosmos DB batch takes ${BATCH_OPERATIONS} operations; this write needs ${planned.length}.`);
|
|
507
510
|
let results;
|
|
508
511
|
try {
|
|
509
|
-
const response = await container().items.batch(planned.map(({ operation }) => operation), COSMOS_PARTITION);
|
|
512
|
+
const response = await (await container()).items.batch(planned.map(({ operation }) => operation), COSMOS_PARTITION);
|
|
510
513
|
if (response.result === void 0) throw Object.assign(/* @__PURE__ */ new Error(`Cosmos DB refused the batch with status ${response.code}`), { code: response.code });
|
|
511
514
|
results = response.result;
|
|
512
515
|
} catch (error) {
|
|
@@ -540,7 +543,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
|
|
|
540
543
|
async deleteConsumed(keys) {
|
|
541
544
|
let remaining = [...new Set(keys.map(cosmosItemId))];
|
|
542
545
|
for (let attempt = 0; remaining.length > 0; attempt += 1) {
|
|
543
|
-
const failed = (await container().items.executeBulkOperations(remaining.map((id) => ({
|
|
546
|
+
const failed = (await (await container()).items.executeBulkOperations(remaining.map((id) => ({
|
|
544
547
|
operationType: "Delete",
|
|
545
548
|
id,
|
|
546
549
|
partitionKey: COSMOS_PARTITION
|
|
@@ -561,8 +564,8 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
|
|
|
561
564
|
migrations: {
|
|
562
565
|
/** Creates the database and the container when they are missing. */
|
|
563
566
|
async apply() {
|
|
564
|
-
if (engine !== void 0) await assertCosmosContainerReusable(container(), engine);
|
|
565
|
-
const { database } = await open().client.databases.createIfNotExists({ id: databaseId });
|
|
567
|
+
if (engine !== void 0) await assertCosmosContainerReusable(await container(), engine);
|
|
568
|
+
const { database } = await (await open()).client.databases.createIfNotExists({ id: databaseId });
|
|
566
569
|
const { resource } = await database.containers.createIfNotExists({
|
|
567
570
|
id: containerId,
|
|
568
571
|
partitionKey: { paths: [COSMOS_PARTITION_KEY_PATH] },
|
|
@@ -599,8 +602,12 @@ const adapterOf = (config) => {
|
|
|
599
602
|
const { databaseId = AZURE_COSMOS_V1_DATABASE, containerId = AZURE_COSMOS_V1_CONTAINER, aggregateBatching: _, ...connection } = config;
|
|
600
603
|
if (connection.consistencyLevel !== void 0 && connection.consistencyLevel !== "Strong") throw new Error(`cosmosDatabase reads with strong consistency; consistencyLevel "${String(connection.consistencyLevel)}" is weaker.`);
|
|
601
604
|
let client;
|
|
605
|
+
const { connectionString } = connection;
|
|
602
606
|
const store = createCosmosStore({
|
|
603
|
-
client: () => client ??= new _azure_cosmos.CosmosClient(
|
|
607
|
+
client: async () => client ??= new _azure_cosmos.CosmosClient({
|
|
608
|
+
...connection,
|
|
609
|
+
connectionString: typeof connectionString === "function" ? await connectionString() : connectionString
|
|
610
|
+
}),
|
|
604
611
|
databaseId,
|
|
605
612
|
containerId,
|
|
606
613
|
engine: currentCosmosEngine()
|
|
@@ -629,7 +636,7 @@ const migrateCosmosDatabase = (config, plugins = []) => {
|
|
|
629
636
|
const DATABASE_TARGET = Symbol.for("@hot-updater/database-target");
|
|
630
637
|
/** The account host a client reaches: its endpoint, or the connection string's. */
|
|
631
638
|
const accountHostOf = ({ endpoint, connectionString }) => {
|
|
632
|
-
const url = endpoint ?? connectionString
|
|
639
|
+
const url = endpoint ?? (typeof connectionString === "string" ? connectionString.match(/AccountEndpoint=([^;]+)/iu)?.[1] : void 0);
|
|
633
640
|
try {
|
|
634
641
|
return url === void 0 ? void 0 : new URL(url).hostname;
|
|
635
642
|
} catch {
|
|
@@ -7451,26 +7458,38 @@ const verifyFunction = async (baseURL, { attempts = 60, delayMs = 5e3 } = {}) =>
|
|
|
7451
7458
|
* import without its extension, so it names the container itself.
|
|
7452
7459
|
*/
|
|
7453
7460
|
const BLOB_CONTAINER = "hot-updater";
|
|
7454
|
-
|
|
7461
|
+
/**
|
|
7462
|
+
* The config init writes. With the `resources` init set up, it names the
|
|
7463
|
+
* Cosmos DB account, and reads both connection strings with `secretEnv`,
|
|
7464
|
+
* from the environment and `.env.hotupdater`, then the config's `secrets`
|
|
7465
|
+
* adapter. Without them, as the agent scaffold writes it, it reads them
|
|
7466
|
+
* from the environment.
|
|
7467
|
+
*/
|
|
7468
|
+
const getConfigScaffold = (build, resources) => {
|
|
7469
|
+
const read = (reader, name) => resources === void 0 ? `process.env.${name}!` : `async () =>
|
|
7470
|
+
(await ${reader}()).${name}`;
|
|
7455
7471
|
const storageConfig = {
|
|
7456
7472
|
imports: [{
|
|
7457
7473
|
pkg: "@hot-updater/azure",
|
|
7458
7474
|
named: ["azureBlobStorage"]
|
|
7459
7475
|
}],
|
|
7460
7476
|
configString: `azureBlobStorage({
|
|
7461
|
-
connectionString:
|
|
7477
|
+
connectionString: ${read("storageSecrets", "HOT_UPDATER_AZURE_STORAGE_CONNECTION_STRING")},
|
|
7462
7478
|
containerName: "${BLOB_CONTAINER}",
|
|
7463
7479
|
})`
|
|
7464
7480
|
};
|
|
7465
|
-
|
|
7481
|
+
const databaseConfig = {
|
|
7466
7482
|
imports: [{
|
|
7467
7483
|
pkg: "@hot-updater/azure",
|
|
7468
7484
|
named: ["cosmosDatabase"]
|
|
7469
7485
|
}],
|
|
7470
|
-
configString: `cosmosDatabase({
|
|
7471
|
-
|
|
7486
|
+
configString: `cosmosDatabase({${resources?.cosmosEndpoint === void 0 ? "" : `
|
|
7487
|
+
endpoint: ${JSON.stringify(resources.cosmosEndpoint)},`}
|
|
7488
|
+
connectionString: ${read("databaseSecrets", "HOT_UPDATER_AZURE_COSMOS_CONNECTION_STRING")},
|
|
7472
7489
|
})`
|
|
7473
|
-
}
|
|
7490
|
+
};
|
|
7491
|
+
const helperStatements = resources === void 0 ? [] : [(0, _hot_updater_cli_tools.secretEnvHelper)("storageSecrets", ["HOT_UPDATER_AZURE_STORAGE_CONNECTION_STRING"]), (0, _hot_updater_cli_tools.secretEnvHelper)("databaseSecrets", ["HOT_UPDATER_AZURE_COSMOS_CONNECTION_STRING"])];
|
|
7492
|
+
const builder = new _hot_updater_cli_tools.ConfigBuilder().setBuildType(build).setStorage(storageConfig).setDatabase(databaseConfig).setPlugins({
|
|
7474
7493
|
imports: [{
|
|
7475
7494
|
pkg: "hot-updater/plugins",
|
|
7476
7495
|
named: [
|
|
@@ -7480,7 +7499,12 @@ const getConfigScaffold = (build) => {
|
|
|
7480
7499
|
]
|
|
7481
7500
|
}],
|
|
7482
7501
|
configString: "[apiKeys(), insights(), remoteConfig()]"
|
|
7483
|
-
})
|
|
7502
|
+
});
|
|
7503
|
+
if (resources !== void 0) builder.addImport({
|
|
7504
|
+
pkg: "hot-updater",
|
|
7505
|
+
named: ["secretEnv"]
|
|
7506
|
+
}).setIntermediateCode(helperStatements.map(({ code }) => code).join("\n\n"));
|
|
7507
|
+
return (0, _hot_updater_cli_tools.createHotUpdaterConfigScaffoldFromBuilder)(builder, { helperStatements });
|
|
7484
7508
|
};
|
|
7485
7509
|
//#endregion
|
|
7486
7510
|
//#region iac/init/index.ts
|
|
@@ -7565,7 +7589,8 @@ const initProvider = {
|
|
|
7565
7589
|
optional: true,
|
|
7566
7590
|
preflight: false
|
|
7567
7591
|
}
|
|
7568
|
-
}
|
|
7592
|
+
},
|
|
7593
|
+
secrets: ["HOT_UPDATER_AZURE_STORAGE_CONNECTION_STRING", "HOT_UPDATER_AZURE_COSMOS_CONNECTION_STRING"]
|
|
7569
7594
|
};
|
|
7570
7595
|
//#endregion
|
|
7571
7596
|
//#region iac/index.ts
|
|
@@ -7625,10 +7650,10 @@ const resolveName = async ({ input, saved, fallback, nonInteractive, validate, h
|
|
|
7625
7650
|
validate: (value) => validate(value) ? void 0 : hint
|
|
7626
7651
|
}));
|
|
7627
7652
|
};
|
|
7628
|
-
const runInit = async ({ build, envFile }) => {
|
|
7653
|
+
const runInit = async ({ build, envFile, secretsEnv }) => {
|
|
7629
7654
|
const cwd = (0, _hot_updater_cli_tools.getCwd)();
|
|
7630
7655
|
const nonInteractive = envFile !== void 0;
|
|
7631
|
-
const initEnvSources = await (0, _hot_updater_cli_tools.readHotUpdaterInitEnv)(cwd, envFile);
|
|
7656
|
+
const initEnvSources = await (0, _hot_updater_cli_tools.readHotUpdaterInitEnv)(cwd, envFile, secretsEnv);
|
|
7632
7657
|
const initInputEnv = (0, _hot_updater_cli_tools.getHotUpdaterInitInputEnv)(initEnvSources, nonInteractive);
|
|
7633
7658
|
const saved = Object.fromEntries(Object.entries(inputs).map(([name, input]) => [name, initInputEnv[input.envKey]?.trim() || void 0]));
|
|
7634
7659
|
const cliVersion = await azureCliVersion();
|
|
@@ -7835,7 +7860,8 @@ const runInit = async ({ build, envFile }) => {
|
|
|
7835
7860
|
await database.dispose?.();
|
|
7836
7861
|
}
|
|
7837
7862
|
_hot_updater_cli_tools.p.log.success("Generated '.env.hotupdater' file with Azure settings.");
|
|
7838
|
-
|
|
7863
|
+
const cosmosEndpoint = /AccountEndpoint=([^;]+)/iu.exec(cosmosConnectionString)?.[1];
|
|
7864
|
+
await (0, _hot_updater_cli_tools.writeHotUpdaterFiles)(getConfigScaffold(build, cosmosEndpoint === void 0 ? {} : { cosmosEndpoint }), {
|
|
7839
7865
|
cwd,
|
|
7840
7866
|
settings: "Azure"
|
|
7841
7867
|
});
|
package/dist/init/index.d.cts
CHANGED
|
@@ -81,6 +81,7 @@ declare const initProvider: {
|
|
|
81
81
|
readonly preflight: false;
|
|
82
82
|
};
|
|
83
83
|
};
|
|
84
|
+
readonly secrets: readonly ["HOT_UPDATER_AZURE_STORAGE_CONNECTION_STRING", "HOT_UPDATER_AZURE_COSMOS_CONNECTION_STRING"];
|
|
84
85
|
};
|
|
85
86
|
//#endregion
|
|
86
87
|
//#region iac/index.d.ts
|
|
@@ -94,6 +95,6 @@ declare const defaultResourceNames: (subscriptionId: string, resourceGroup: stri
|
|
|
94
95
|
cosmosAccount: string;
|
|
95
96
|
functionApp: string;
|
|
96
97
|
};
|
|
97
|
-
declare const runInit: ({ build, envFile }: RunInitOptions) => Promise<void>;
|
|
98
|
+
declare const runInit: ({ build, envFile, secretsEnv }: RunInitOptions) => Promise<void>;
|
|
98
99
|
//#endregion
|
|
99
100
|
export { AZURE_FUNCTION_PACKAGE, defaultResourceNames, initProvider, runInit };
|
package/dist/init/index.d.mts
CHANGED
|
@@ -81,6 +81,7 @@ declare const initProvider: {
|
|
|
81
81
|
readonly preflight: false;
|
|
82
82
|
};
|
|
83
83
|
};
|
|
84
|
+
readonly secrets: readonly ["HOT_UPDATER_AZURE_STORAGE_CONNECTION_STRING", "HOT_UPDATER_AZURE_COSMOS_CONNECTION_STRING"];
|
|
84
85
|
};
|
|
85
86
|
//#endregion
|
|
86
87
|
//#region iac/index.d.ts
|
|
@@ -94,6 +95,6 @@ declare const defaultResourceNames: (subscriptionId: string, resourceGroup: stri
|
|
|
94
95
|
cosmosAccount: string;
|
|
95
96
|
functionApp: string;
|
|
96
97
|
};
|
|
97
|
-
declare const runInit: ({ build, envFile }: RunInitOptions) => Promise<void>;
|
|
98
|
+
declare const runInit: ({ build, envFile, secretsEnv }: RunInitOptions) => Promise<void>;
|
|
98
99
|
//#endregion
|
|
99
100
|
export { AZURE_FUNCTION_PACKAGE, defaultResourceNames, initProvider, runInit };
|
package/dist/init/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { createHash } from "crypto";
|
|
3
3
|
import path from "path";
|
|
4
|
-
import { ConfigBuilder, InitError, assembleServer, createHotUpdaterConfigScaffoldFromBuilder, createZip, getCwd, getHotUpdaterInitInputEnv, getInitProviderEnvVars, getInitProviderTextPromptValues, link, makeEnv, p, printAppSetup, provisionClientCredential, readHotUpdaterInitEnv, shouldAutoSelectOnlyInitResource, writeHotUpdaterFiles } from "@hot-updater/cli-tools";
|
|
4
|
+
import { ConfigBuilder, InitError, assembleServer, createHotUpdaterConfigScaffoldFromBuilder, createZip, getCwd, getHotUpdaterInitInputEnv, getInitProviderEnvVars, getInitProviderTextPromptValues, link, makeEnv, p, printAppSetup, provisionClientCredential, readHotUpdaterInitEnv, secretEnvHelper, shouldAutoSelectOnlyInitResource, writeHotUpdaterFiles } from "@hot-updater/cli-tools";
|
|
5
5
|
import { apiKeys, insights, remoteConfig } from "@hot-updater/server/plugins";
|
|
6
6
|
import { CosmosClient } from "@azure/cosmos";
|
|
7
7
|
import { SETTINGS_TABLE, createEngineDatabase, createKvAdapter, encodeKvKey, migrateCoreSchema, toolingTargetOf } from "@hot-updater/plugin-core";
|
|
@@ -262,14 +262,17 @@ const emulatorAborted = (error) => statusOf(error) === 500 && /PostgresError\(Sq
|
|
|
262
262
|
*/
|
|
263
263
|
const createCosmosStore = ({ client, databaseId, containerId, engine, nativePageSize }) => {
|
|
264
264
|
let opened;
|
|
265
|
-
const open = () => opened ??= (() => {
|
|
266
|
-
const account = typeof client === "function" ? client() : client;
|
|
265
|
+
const open = () => opened ??= (async () => {
|
|
266
|
+
const account = typeof client === "function" ? await client() : client;
|
|
267
267
|
return {
|
|
268
268
|
client: account,
|
|
269
269
|
container: account.database(databaseId).container(containerId)
|
|
270
270
|
};
|
|
271
|
-
})()
|
|
272
|
-
|
|
271
|
+
})().catch((error) => {
|
|
272
|
+
opened = void 0;
|
|
273
|
+
throw error;
|
|
274
|
+
});
|
|
275
|
+
const container = async () => (await open()).container;
|
|
273
276
|
/**
|
|
274
277
|
* The state of the items `ids` names, by id, with the rows of `rowIds`;
|
|
275
278
|
* a missing item has none.
|
|
@@ -277,7 +280,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
|
|
|
277
280
|
const readStates = async (ids, rowIds) => {
|
|
278
281
|
const states = /* @__PURE__ */ new Map();
|
|
279
282
|
for (const [group, projection] of [[ids.filter((id) => !rowIds.has(id)), ""], [ids.filter((id) => rowIds.has(id)), ", c.row"]]) for (let at = 0; at < group.length; at += GET_BATCH) {
|
|
280
|
-
const { resources } = await container().items.query(`SELECT c.id, c._etag AS etag, c.row._v AS v, c.expireAt${projection} FROM c WHERE c.id IN (${idList(group.slice(at, at + GET_BATCH))})`, { partitionKey: COSMOS_PARTITION }).fetchAll().catch(missingContainer);
|
|
283
|
+
const { resources } = await (await container()).items.query(`SELECT c.id, c._etag AS etag, c.row._v AS v, c.expireAt${projection} FROM c WHERE c.id IN (${idList(group.slice(at, at + GET_BATCH))})`, { partitionKey: COSMOS_PARTITION }).fetchAll().catch(missingContainer);
|
|
281
284
|
for (const { id, etag, v, expireAt, row } of resources) states.set(id, {
|
|
282
285
|
etag,
|
|
283
286
|
v,
|
|
@@ -415,7 +418,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
|
|
|
415
418
|
const found = /* @__PURE__ */ new Map();
|
|
416
419
|
const ids = [...new Set(keys.map(cosmosItemId))];
|
|
417
420
|
for (let at = 0; at < ids.length; at += GET_BATCH) {
|
|
418
|
-
const { resources } = await container().items.query(`SELECT c.id, c.row FROM c WHERE c.id IN (${idList(ids.slice(at, at + GET_BATCH))})`, { partitionKey: COSMOS_PARTITION }).fetchAll().catch(missingContainer);
|
|
421
|
+
const { resources } = await (await container()).items.query(`SELECT c.id, c.row FROM c WHERE c.id IN (${idList(ids.slice(at, at + GET_BATCH))})`, { partitionKey: COSMOS_PARTITION }).fetchAll().catch(missingContainer);
|
|
419
422
|
for (const resource of resources) found.set(resource.id, toRow(resource.row));
|
|
420
423
|
}
|
|
421
424
|
return keys.map((key) => found.get(cosmosItemId(key)) ?? null);
|
|
@@ -451,7 +454,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
|
|
|
451
454
|
value: after
|
|
452
455
|
});
|
|
453
456
|
}
|
|
454
|
-
const { resources } = await container().items.query({
|
|
457
|
+
const { resources } = await (await container()).items.query({
|
|
455
458
|
query: `SELECT TOP @size c.sk, c.row FROM c WHERE ${filters.join(" AND ")} ORDER BY c.pk ${order === "asc" ? "ASC" : "DESC"}, c.sk ${order === "asc" ? "ASC" : "DESC"}`,
|
|
456
459
|
parameters
|
|
457
460
|
}, {
|
|
@@ -501,7 +504,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
|
|
|
501
504
|
if (planned.length > BATCH_OPERATIONS) throw new Error(`A Cosmos DB batch takes ${BATCH_OPERATIONS} operations; this write needs ${planned.length}.`);
|
|
502
505
|
let results;
|
|
503
506
|
try {
|
|
504
|
-
const response = await container().items.batch(planned.map(({ operation }) => operation), COSMOS_PARTITION);
|
|
507
|
+
const response = await (await container()).items.batch(planned.map(({ operation }) => operation), COSMOS_PARTITION);
|
|
505
508
|
if (response.result === void 0) throw Object.assign(/* @__PURE__ */ new Error(`Cosmos DB refused the batch with status ${response.code}`), { code: response.code });
|
|
506
509
|
results = response.result;
|
|
507
510
|
} catch (error) {
|
|
@@ -535,7 +538,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
|
|
|
535
538
|
async deleteConsumed(keys) {
|
|
536
539
|
let remaining = [...new Set(keys.map(cosmosItemId))];
|
|
537
540
|
for (let attempt = 0; remaining.length > 0; attempt += 1) {
|
|
538
|
-
const failed = (await container().items.executeBulkOperations(remaining.map((id) => ({
|
|
541
|
+
const failed = (await (await container()).items.executeBulkOperations(remaining.map((id) => ({
|
|
539
542
|
operationType: "Delete",
|
|
540
543
|
id,
|
|
541
544
|
partitionKey: COSMOS_PARTITION
|
|
@@ -556,8 +559,8 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
|
|
|
556
559
|
migrations: {
|
|
557
560
|
/** Creates the database and the container when they are missing. */
|
|
558
561
|
async apply() {
|
|
559
|
-
if (engine !== void 0) await assertCosmosContainerReusable(container(), engine);
|
|
560
|
-
const { database } = await open().client.databases.createIfNotExists({ id: databaseId });
|
|
562
|
+
if (engine !== void 0) await assertCosmosContainerReusable(await container(), engine);
|
|
563
|
+
const { database } = await (await open()).client.databases.createIfNotExists({ id: databaseId });
|
|
561
564
|
const { resource } = await database.containers.createIfNotExists({
|
|
562
565
|
id: containerId,
|
|
563
566
|
partitionKey: { paths: [COSMOS_PARTITION_KEY_PATH] },
|
|
@@ -594,8 +597,12 @@ const adapterOf = (config) => {
|
|
|
594
597
|
const { databaseId = AZURE_COSMOS_V1_DATABASE, containerId = AZURE_COSMOS_V1_CONTAINER, aggregateBatching: _, ...connection } = config;
|
|
595
598
|
if (connection.consistencyLevel !== void 0 && connection.consistencyLevel !== "Strong") throw new Error(`cosmosDatabase reads with strong consistency; consistencyLevel "${String(connection.consistencyLevel)}" is weaker.`);
|
|
596
599
|
let client;
|
|
600
|
+
const { connectionString } = connection;
|
|
597
601
|
const store = createCosmosStore({
|
|
598
|
-
client: () => client ??= new CosmosClient(
|
|
602
|
+
client: async () => client ??= new CosmosClient({
|
|
603
|
+
...connection,
|
|
604
|
+
connectionString: typeof connectionString === "function" ? await connectionString() : connectionString
|
|
605
|
+
}),
|
|
599
606
|
databaseId,
|
|
600
607
|
containerId,
|
|
601
608
|
engine: currentCosmosEngine()
|
|
@@ -624,7 +631,7 @@ const migrateCosmosDatabase = (config, plugins = []) => {
|
|
|
624
631
|
const DATABASE_TARGET = Symbol.for("@hot-updater/database-target");
|
|
625
632
|
/** The account host a client reaches: its endpoint, or the connection string's. */
|
|
626
633
|
const accountHostOf = ({ endpoint, connectionString }) => {
|
|
627
|
-
const url = endpoint ?? connectionString
|
|
634
|
+
const url = endpoint ?? (typeof connectionString === "string" ? connectionString.match(/AccountEndpoint=([^;]+)/iu)?.[1] : void 0);
|
|
628
635
|
try {
|
|
629
636
|
return url === void 0 ? void 0 : new URL(url).hostname;
|
|
630
637
|
} catch {
|
|
@@ -7446,26 +7453,38 @@ const verifyFunction = async (baseURL, { attempts = 60, delayMs = 5e3 } = {}) =>
|
|
|
7446
7453
|
* import without its extension, so it names the container itself.
|
|
7447
7454
|
*/
|
|
7448
7455
|
const BLOB_CONTAINER = "hot-updater";
|
|
7449
|
-
|
|
7456
|
+
/**
|
|
7457
|
+
* The config init writes. With the `resources` init set up, it names the
|
|
7458
|
+
* Cosmos DB account, and reads both connection strings with `secretEnv`,
|
|
7459
|
+
* from the environment and `.env.hotupdater`, then the config's `secrets`
|
|
7460
|
+
* adapter. Without them, as the agent scaffold writes it, it reads them
|
|
7461
|
+
* from the environment.
|
|
7462
|
+
*/
|
|
7463
|
+
const getConfigScaffold = (build, resources) => {
|
|
7464
|
+
const read = (reader, name) => resources === void 0 ? `process.env.${name}!` : `async () =>
|
|
7465
|
+
(await ${reader}()).${name}`;
|
|
7450
7466
|
const storageConfig = {
|
|
7451
7467
|
imports: [{
|
|
7452
7468
|
pkg: "@hot-updater/azure",
|
|
7453
7469
|
named: ["azureBlobStorage"]
|
|
7454
7470
|
}],
|
|
7455
7471
|
configString: `azureBlobStorage({
|
|
7456
|
-
connectionString:
|
|
7472
|
+
connectionString: ${read("storageSecrets", "HOT_UPDATER_AZURE_STORAGE_CONNECTION_STRING")},
|
|
7457
7473
|
containerName: "${BLOB_CONTAINER}",
|
|
7458
7474
|
})`
|
|
7459
7475
|
};
|
|
7460
|
-
|
|
7476
|
+
const databaseConfig = {
|
|
7461
7477
|
imports: [{
|
|
7462
7478
|
pkg: "@hot-updater/azure",
|
|
7463
7479
|
named: ["cosmosDatabase"]
|
|
7464
7480
|
}],
|
|
7465
|
-
configString: `cosmosDatabase({
|
|
7466
|
-
|
|
7481
|
+
configString: `cosmosDatabase({${resources?.cosmosEndpoint === void 0 ? "" : `
|
|
7482
|
+
endpoint: ${JSON.stringify(resources.cosmosEndpoint)},`}
|
|
7483
|
+
connectionString: ${read("databaseSecrets", "HOT_UPDATER_AZURE_COSMOS_CONNECTION_STRING")},
|
|
7467
7484
|
})`
|
|
7468
|
-
}
|
|
7485
|
+
};
|
|
7486
|
+
const helperStatements = resources === void 0 ? [] : [secretEnvHelper("storageSecrets", ["HOT_UPDATER_AZURE_STORAGE_CONNECTION_STRING"]), secretEnvHelper("databaseSecrets", ["HOT_UPDATER_AZURE_COSMOS_CONNECTION_STRING"])];
|
|
7487
|
+
const builder = new ConfigBuilder().setBuildType(build).setStorage(storageConfig).setDatabase(databaseConfig).setPlugins({
|
|
7469
7488
|
imports: [{
|
|
7470
7489
|
pkg: "hot-updater/plugins",
|
|
7471
7490
|
named: [
|
|
@@ -7475,7 +7494,12 @@ const getConfigScaffold = (build) => {
|
|
|
7475
7494
|
]
|
|
7476
7495
|
}],
|
|
7477
7496
|
configString: "[apiKeys(), insights(), remoteConfig()]"
|
|
7478
|
-
})
|
|
7497
|
+
});
|
|
7498
|
+
if (resources !== void 0) builder.addImport({
|
|
7499
|
+
pkg: "hot-updater",
|
|
7500
|
+
named: ["secretEnv"]
|
|
7501
|
+
}).setIntermediateCode(helperStatements.map(({ code }) => code).join("\n\n"));
|
|
7502
|
+
return createHotUpdaterConfigScaffoldFromBuilder(builder, { helperStatements });
|
|
7479
7503
|
};
|
|
7480
7504
|
//#endregion
|
|
7481
7505
|
//#region iac/init/index.ts
|
|
@@ -7560,7 +7584,8 @@ const initProvider = {
|
|
|
7560
7584
|
optional: true,
|
|
7561
7585
|
preflight: false
|
|
7562
7586
|
}
|
|
7563
|
-
}
|
|
7587
|
+
},
|
|
7588
|
+
secrets: ["HOT_UPDATER_AZURE_STORAGE_CONNECTION_STRING", "HOT_UPDATER_AZURE_COSMOS_CONNECTION_STRING"]
|
|
7564
7589
|
};
|
|
7565
7590
|
//#endregion
|
|
7566
7591
|
//#region iac/index.ts
|
|
@@ -7620,10 +7645,10 @@ const resolveName = async ({ input, saved, fallback, nonInteractive, validate, h
|
|
|
7620
7645
|
validate: (value) => validate(value) ? void 0 : hint
|
|
7621
7646
|
}));
|
|
7622
7647
|
};
|
|
7623
|
-
const runInit = async ({ build, envFile }) => {
|
|
7648
|
+
const runInit = async ({ build, envFile, secretsEnv }) => {
|
|
7624
7649
|
const cwd = getCwd();
|
|
7625
7650
|
const nonInteractive = envFile !== void 0;
|
|
7626
|
-
const initEnvSources = await readHotUpdaterInitEnv(cwd, envFile);
|
|
7651
|
+
const initEnvSources = await readHotUpdaterInitEnv(cwd, envFile, secretsEnv);
|
|
7627
7652
|
const initInputEnv = getHotUpdaterInitInputEnv(initEnvSources, nonInteractive);
|
|
7628
7653
|
const saved = Object.fromEntries(Object.entries(inputs).map(([name, input]) => [name, initInputEnv[input.envKey]?.trim() || void 0]));
|
|
7629
7654
|
const cliVersion = await azureCliVersion();
|
|
@@ -7830,7 +7855,8 @@ const runInit = async ({ build, envFile }) => {
|
|
|
7830
7855
|
await database.dispose?.();
|
|
7831
7856
|
}
|
|
7832
7857
|
p.log.success("Generated '.env.hotupdater' file with Azure settings.");
|
|
7833
|
-
|
|
7858
|
+
const cosmosEndpoint = /AccountEndpoint=([^;]+)/iu.exec(cosmosConnectionString)?.[1];
|
|
7859
|
+
await writeHotUpdaterFiles(getConfigScaffold(build, cosmosEndpoint === void 0 ? {} : { cosmosEndpoint }), {
|
|
7834
7860
|
cwd,
|
|
7835
7861
|
settings: "Azure"
|
|
7836
7862
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hot-updater/azure",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.2.0-rc.
|
|
4
|
+
"version": "1.2.0-rc.6",
|
|
5
5
|
"description": "React Native OTA solution for self-hosted",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "^22.23.4 || ^24.22.1 || >=26.12.1"
|
|
@@ -42,10 +42,10 @@
|
|
|
42
42
|
"@azure/core-auth": "1.11.0",
|
|
43
43
|
"@azure/cosmos": "4.10.1",
|
|
44
44
|
"@azure/storage-blob": "12.34.0",
|
|
45
|
-
"@hot-updater/
|
|
46
|
-
"@hot-updater/
|
|
47
|
-
"@hot-updater/
|
|
48
|
-
"@hot-updater/
|
|
45
|
+
"@hot-updater/cli-tools": "1.2.0-rc.6",
|
|
46
|
+
"@hot-updater/plugin-core": "1.2.0-rc.6",
|
|
47
|
+
"@hot-updater/protocol": "1.2.0-rc.6",
|
|
48
|
+
"@hot-updater/server": "1.2.0-rc.6"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@azure/functions": "4.16.5",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"execa": "9.5.2",
|
|
54
54
|
"jszip": "^3.10.1",
|
|
55
55
|
"vitest": "4.1.4",
|
|
56
|
-
"@hot-updater/test-utils": "1.2.0-rc.
|
|
56
|
+
"@hot-updater/test-utils": "1.2.0-rc.6"
|
|
57
57
|
},
|
|
58
58
|
"scripts": {
|
|
59
59
|
"build": "tsdown",
|