@hot-updater/azure 1.1.4 → 1.2.0-rc.0
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 +894 -418
- package/dist/index.cjs +73 -28
- package/dist/index.mjs +73 -28
- package/dist/init/index.cjs +40 -15
- package/dist/init/index.mjs +40 -15
- package/package.json +6 -6
package/dist/index.cjs
CHANGED
|
@@ -20,8 +20,9 @@ const serviceOf = (config) => "connectionString" in config ? _azure_storage_blob
|
|
|
20
20
|
*/
|
|
21
21
|
function azureBlobStorage(config) {
|
|
22
22
|
const { containerName, basePath, getDownloadUrl } = config;
|
|
23
|
-
|
|
24
|
-
const
|
|
23
|
+
let client;
|
|
24
|
+
const service = () => client ??= serviceOf(config);
|
|
25
|
+
const container = () => service().getContainerClient(containerName);
|
|
25
26
|
const normalizedBasePath = basePath?.replace(/^\/+|\/+$/g, "") ?? "";
|
|
26
27
|
const getStorageKey = (0, _hot_updater_plugin_core.createStorageKeyBuilder)(normalizedBasePath);
|
|
27
28
|
const getListPrefix = (prefix = "") => {
|
|
@@ -53,7 +54,7 @@ function azureBlobStorage(config) {
|
|
|
53
54
|
const delegationKey = () => {
|
|
54
55
|
const now = Date.now();
|
|
55
56
|
if (delegation === void 0 || delegation.renewAt <= now) {
|
|
56
|
-
const key = service.getUserDelegationKey(/* @__PURE__ */ new Date(now - 3e5), new Date(now + DELEGATION_KEY_LIFETIME_MS));
|
|
57
|
+
const key = service().getUserDelegationKey(/* @__PURE__ */ new Date(now - 3e5), new Date(now + DELEGATION_KEY_LIFETIME_MS));
|
|
57
58
|
key.catch(() => {
|
|
58
59
|
delegation = void 0;
|
|
59
60
|
});
|
|
@@ -65,7 +66,7 @@ function azureBlobStorage(config) {
|
|
|
65
66
|
return delegation.key;
|
|
66
67
|
};
|
|
67
68
|
const signedUrl = async (key) => {
|
|
68
|
-
const blob = container.getBlobClient(key);
|
|
69
|
+
const blob = container().getBlobClient(key);
|
|
69
70
|
const expiresOn = new Date(Date.now() + SAS_EXPIRES_IN_MS);
|
|
70
71
|
const permissions = _azure_storage_blob.BlobSASPermissions.parse("r");
|
|
71
72
|
if (blob.credential instanceof _azure_storage_blob.StorageSharedKeyCredential) return blob.generateSasUrl({
|
|
@@ -86,7 +87,7 @@ function azureBlobStorage(config) {
|
|
|
86
87
|
protocol: PROTOCOL,
|
|
87
88
|
async listObjects(prefix) {
|
|
88
89
|
const objects = [];
|
|
89
|
-
for await (const blob of container.listBlobsFlat({ prefix: getListPrefix(prefix) })) {
|
|
90
|
+
for await (const blob of container().listBlobsFlat({ prefix: getListPrefix(prefix) })) {
|
|
90
91
|
const storageUri = getListedStorageUri(blob.name);
|
|
91
92
|
if (storageUri === null) continue;
|
|
92
93
|
objects.push({
|
|
@@ -103,17 +104,36 @@ function azureBlobStorage(config) {
|
|
|
103
104
|
for (let offset = 0; offset < keys.length; offset += 256) {
|
|
104
105
|
const batch = keys.slice(offset, offset + 256);
|
|
105
106
|
if (batch.length === 0) continue;
|
|
106
|
-
const failed = (await container.getBlobBatchClient().deleteBlobs(batch.map((key) => container.getBlobClient(key)))).subResponses.filter(({ status }) => status >= 300 && status !== 404);
|
|
107
|
+
const failed = (await container().getBlobBatchClient().deleteBlobs(batch.map((key) => container().getBlobClient(key)))).subResponses.filter(({ status }) => status >= 300 && status !== 404);
|
|
107
108
|
if (failed.length > 0) throw new Error(`Failed to delete ${failed.length} Azure blob(s).`);
|
|
108
109
|
}
|
|
109
110
|
},
|
|
110
|
-
async put({ key, body, contentType }) {
|
|
111
|
+
async put({ key, body, contentLength, contentType }) {
|
|
111
112
|
const storageKey = getStorageKey(key);
|
|
112
|
-
const
|
|
113
|
-
await container.getBlockBlobClient(storageKey).uploadData(bytes, { blobHTTPHeaders: {
|
|
113
|
+
const options = { blobHTTPHeaders: {
|
|
114
114
|
blobContentType: contentType,
|
|
115
115
|
blobCacheControl: "max-age=31536000"
|
|
116
|
-
} }
|
|
116
|
+
} };
|
|
117
|
+
const blob = container().getBlockBlobClient(storageKey);
|
|
118
|
+
if (contentLength === void 0) await blob.uploadData(new Uint8Array(await new Response(body).arrayBuffer()), options);
|
|
119
|
+
else {
|
|
120
|
+
const { Readable } = await import("node:stream");
|
|
121
|
+
const source = Readable.fromWeb(body);
|
|
122
|
+
const aborted = new AbortController();
|
|
123
|
+
let failure;
|
|
124
|
+
source.once("error", (error) => {
|
|
125
|
+
failure = error;
|
|
126
|
+
aborted.abort(error);
|
|
127
|
+
});
|
|
128
|
+
try {
|
|
129
|
+
await blob.upload(source, contentLength, {
|
|
130
|
+
...options,
|
|
131
|
+
abortSignal: aborted.signal
|
|
132
|
+
});
|
|
133
|
+
} catch (error) {
|
|
134
|
+
throw failure ?? error;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
117
137
|
return { storageUri: (0, _hot_updater_plugin_core.createStorageUri)({
|
|
118
138
|
bucket: containerName,
|
|
119
139
|
key: storageKey,
|
|
@@ -123,7 +143,7 @@ function azureBlobStorage(config) {
|
|
|
123
143
|
async get({ storageUri }) {
|
|
124
144
|
const { key } = parseAndValidate(storageUri);
|
|
125
145
|
try {
|
|
126
|
-
const download = await container.getBlobClient(key).download();
|
|
146
|
+
const download = await container().getBlobClient(key).download();
|
|
127
147
|
const body = download.readableStreamBody;
|
|
128
148
|
if (body === void 0) return { response: null };
|
|
129
149
|
const headers = new Headers();
|
|
@@ -143,11 +163,11 @@ function azureBlobStorage(config) {
|
|
|
143
163
|
},
|
|
144
164
|
async exists({ storageUri }) {
|
|
145
165
|
const { key } = parseAndValidate(storageUri);
|
|
146
|
-
return { exists: await container.getBlobClient(key).exists() };
|
|
166
|
+
return { exists: await container().getBlobClient(key).exists() };
|
|
147
167
|
},
|
|
148
168
|
async delete({ storageUri }) {
|
|
149
169
|
const { key } = parseAndValidate(storageUri);
|
|
150
|
-
await container.getBlobClient(key).deleteIfExists();
|
|
170
|
+
await container().getBlobClient(key).deleteIfExists();
|
|
151
171
|
return { deleted: true };
|
|
152
172
|
}
|
|
153
173
|
});
|
|
@@ -364,7 +384,15 @@ const emulatorAborted = (error) => statusOf(error) === 500 && /PostgresError\(Sq
|
|
|
364
384
|
* replaces the row it read.
|
|
365
385
|
*/
|
|
366
386
|
const createCosmosStore = ({ client, databaseId, containerId, engine, nativePageSize }) => {
|
|
367
|
-
|
|
387
|
+
let opened;
|
|
388
|
+
const open = () => opened ??= (() => {
|
|
389
|
+
const account = typeof client === "function" ? client() : client;
|
|
390
|
+
return {
|
|
391
|
+
client: account,
|
|
392
|
+
container: account.database(databaseId).container(containerId)
|
|
393
|
+
};
|
|
394
|
+
})();
|
|
395
|
+
const container = () => open().container;
|
|
368
396
|
/**
|
|
369
397
|
* The state of the items `ids` names, by id, with the rows of `rowIds`;
|
|
370
398
|
* a missing item has none.
|
|
@@ -372,7 +400,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
|
|
|
372
400
|
const readStates = async (ids, rowIds) => {
|
|
373
401
|
const states = /* @__PURE__ */ new Map();
|
|
374
402
|
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) {
|
|
375
|
-
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);
|
|
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);
|
|
376
404
|
for (const { id, etag, v, expireAt, row } of resources) states.set(id, {
|
|
377
405
|
etag,
|
|
378
406
|
v,
|
|
@@ -510,7 +538,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
|
|
|
510
538
|
const found = /* @__PURE__ */ new Map();
|
|
511
539
|
const ids = [...new Set(keys.map(cosmosItemId))];
|
|
512
540
|
for (let at = 0; at < ids.length; at += GET_BATCH) {
|
|
513
|
-
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);
|
|
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);
|
|
514
542
|
for (const resource of resources) found.set(resource.id, toRow(resource.row));
|
|
515
543
|
}
|
|
516
544
|
return keys.map((key) => found.get(cosmosItemId(key)) ?? null);
|
|
@@ -546,7 +574,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
|
|
|
546
574
|
value: after
|
|
547
575
|
});
|
|
548
576
|
}
|
|
549
|
-
const { resources } = await container.items.query({
|
|
577
|
+
const { resources } = await container().items.query({
|
|
550
578
|
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"}`,
|
|
551
579
|
parameters
|
|
552
580
|
}, {
|
|
@@ -596,7 +624,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
|
|
|
596
624
|
if (planned.length > BATCH_OPERATIONS) throw new Error(`A Cosmos DB batch takes ${BATCH_OPERATIONS} operations; this write needs ${planned.length}.`);
|
|
597
625
|
let results;
|
|
598
626
|
try {
|
|
599
|
-
const response = await container.items.batch(planned.map(({ operation }) => operation), COSMOS_PARTITION);
|
|
627
|
+
const response = await container().items.batch(planned.map(({ operation }) => operation), COSMOS_PARTITION);
|
|
600
628
|
if (response.result === void 0) throw Object.assign(/* @__PURE__ */ new Error(`Cosmos DB refused the batch with status ${response.code}`), { code: response.code });
|
|
601
629
|
results = response.result;
|
|
602
630
|
} catch (error) {
|
|
@@ -630,7 +658,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
|
|
|
630
658
|
async deleteConsumed(keys) {
|
|
631
659
|
let remaining = [...new Set(keys.map(cosmosItemId))];
|
|
632
660
|
for (let attempt = 0; remaining.length > 0; attempt += 1) {
|
|
633
|
-
const failed = (await container.items.executeBulkOperations(remaining.map((id) => ({
|
|
661
|
+
const failed = (await container().items.executeBulkOperations(remaining.map((id) => ({
|
|
634
662
|
operationType: "Delete",
|
|
635
663
|
id,
|
|
636
664
|
partitionKey: COSMOS_PARTITION
|
|
@@ -651,8 +679,8 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
|
|
|
651
679
|
migrations: {
|
|
652
680
|
/** Creates the database and the container when they are missing. */
|
|
653
681
|
async apply() {
|
|
654
|
-
if (engine !== void 0) await assertCosmosContainerReusable(container, engine);
|
|
655
|
-
const { database } = await client.databases.createIfNotExists({ id: databaseId });
|
|
682
|
+
if (engine !== void 0) await assertCosmosContainerReusable(container(), engine);
|
|
683
|
+
const { database } = await open().client.databases.createIfNotExists({ id: databaseId });
|
|
656
684
|
const { resource } = await database.containers.createIfNotExists({
|
|
657
685
|
id: containerId,
|
|
658
686
|
partitionKey: { paths: [COSMOS_PARTITION_KEY_PATH] },
|
|
@@ -688,15 +716,15 @@ async apply() {
|
|
|
688
716
|
const adapterOf = (config) => {
|
|
689
717
|
const { databaseId = AZURE_COSMOS_V1_DATABASE, containerId = AZURE_COSMOS_V1_CONTAINER, aggregateBatching: _, ...connection } = config;
|
|
690
718
|
if (connection.consistencyLevel !== void 0 && connection.consistencyLevel !== "Strong") throw new Error(`cosmosDatabase reads with strong consistency; consistencyLevel "${String(connection.consistencyLevel)}" is weaker.`);
|
|
691
|
-
|
|
719
|
+
let client;
|
|
692
720
|
const store = createCosmosStore({
|
|
693
|
-
client,
|
|
721
|
+
client: () => client ??= new _azure_cosmos.CosmosClient(connection),
|
|
694
722
|
databaseId,
|
|
695
723
|
containerId,
|
|
696
724
|
engine: currentCosmosEngine()
|
|
697
725
|
});
|
|
698
726
|
return {
|
|
699
|
-
client,
|
|
727
|
+
dispose: () => client?.dispose(),
|
|
700
728
|
adapter: (0, _hot_updater_plugin_core.createKvAdapter)({ store })
|
|
701
729
|
};
|
|
702
730
|
};
|
|
@@ -708,8 +736,23 @@ const adapterOf = (config) => {
|
|
|
708
736
|
* it with the managed server's plugins.
|
|
709
737
|
*/
|
|
710
738
|
const migrateCosmosDatabase = (config, plugins = []) => {
|
|
711
|
-
const {
|
|
712
|
-
return (0, _hot_updater_plugin_core.migrateCoreSchema)(adapter, "cosmosDatabase", plugins).finally(
|
|
739
|
+
const { dispose, adapter } = adapterOf(config);
|
|
740
|
+
return (0, _hot_updater_plugin_core.migrateCoreSchema)(adapter, "cosmosDatabase", plugins).finally(dispose);
|
|
741
|
+
};
|
|
742
|
+
/**
|
|
743
|
+
* The resource this database reaches, without credentials, under the key
|
|
744
|
+
* `hot-updater doctor` reads to tell the database `hot-updater init` saved
|
|
745
|
+
* from another (a convention CLAUDE.md documents).
|
|
746
|
+
*/
|
|
747
|
+
const DATABASE_TARGET = Symbol.for("@hot-updater/database-target");
|
|
748
|
+
/** The account host a client reaches: its endpoint, or the connection string's. */
|
|
749
|
+
const accountHostOf = ({ endpoint, connectionString }) => {
|
|
750
|
+
const url = endpoint ?? connectionString?.match(/AccountEndpoint=([^;]+)/iu)?.[1];
|
|
751
|
+
try {
|
|
752
|
+
return url === void 0 ? void 0 : new URL(url).hostname;
|
|
753
|
+
} catch {
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
713
756
|
};
|
|
714
757
|
/**
|
|
715
758
|
* Hot Updater's database in one Azure Cosmos DB for NoSQL container,
|
|
@@ -718,16 +761,18 @@ const migrateCosmosDatabase = (config, plugins = []) => {
|
|
|
718
761
|
* 20 GB.
|
|
719
762
|
*/
|
|
720
763
|
const cosmosDatabase = (config) => {
|
|
721
|
-
const {
|
|
764
|
+
const { dispose, adapter } = adapterOf(config);
|
|
765
|
+
const host = accountHostOf(config);
|
|
722
766
|
return {
|
|
723
767
|
...(0, _hot_updater_plugin_core.createEngineDatabase)({
|
|
724
768
|
name: "cosmosDatabase",
|
|
725
769
|
adapter,
|
|
726
770
|
aggregateBatching: config.aggregateBatching ?? {}
|
|
727
771
|
}),
|
|
772
|
+
...host === void 0 ? {} : { [DATABASE_TARGET]: `cosmos/${host}` },
|
|
728
773
|
async dispose() {
|
|
729
774
|
await adapter.dispose?.();
|
|
730
|
-
|
|
775
|
+
dispose();
|
|
731
776
|
}
|
|
732
777
|
};
|
|
733
778
|
};
|
package/dist/index.mjs
CHANGED
|
@@ -19,8 +19,9 @@ const serviceOf = (config) => "connectionString" in config ? BlobServiceClient.f
|
|
|
19
19
|
*/
|
|
20
20
|
function azureBlobStorage(config) {
|
|
21
21
|
const { containerName, basePath, getDownloadUrl } = config;
|
|
22
|
-
|
|
23
|
-
const
|
|
22
|
+
let client;
|
|
23
|
+
const service = () => client ??= serviceOf(config);
|
|
24
|
+
const container = () => service().getContainerClient(containerName);
|
|
24
25
|
const normalizedBasePath = basePath?.replace(/^\/+|\/+$/g, "") ?? "";
|
|
25
26
|
const getStorageKey = createStorageKeyBuilder(normalizedBasePath);
|
|
26
27
|
const getListPrefix = (prefix = "") => {
|
|
@@ -52,7 +53,7 @@ function azureBlobStorage(config) {
|
|
|
52
53
|
const delegationKey = () => {
|
|
53
54
|
const now = Date.now();
|
|
54
55
|
if (delegation === void 0 || delegation.renewAt <= now) {
|
|
55
|
-
const key = service.getUserDelegationKey(/* @__PURE__ */ new Date(now - 3e5), new Date(now + DELEGATION_KEY_LIFETIME_MS));
|
|
56
|
+
const key = service().getUserDelegationKey(/* @__PURE__ */ new Date(now - 3e5), new Date(now + DELEGATION_KEY_LIFETIME_MS));
|
|
56
57
|
key.catch(() => {
|
|
57
58
|
delegation = void 0;
|
|
58
59
|
});
|
|
@@ -64,7 +65,7 @@ function azureBlobStorage(config) {
|
|
|
64
65
|
return delegation.key;
|
|
65
66
|
};
|
|
66
67
|
const signedUrl = async (key) => {
|
|
67
|
-
const blob = container.getBlobClient(key);
|
|
68
|
+
const blob = container().getBlobClient(key);
|
|
68
69
|
const expiresOn = new Date(Date.now() + SAS_EXPIRES_IN_MS);
|
|
69
70
|
const permissions = BlobSASPermissions.parse("r");
|
|
70
71
|
if (blob.credential instanceof StorageSharedKeyCredential) return blob.generateSasUrl({
|
|
@@ -85,7 +86,7 @@ function azureBlobStorage(config) {
|
|
|
85
86
|
protocol: PROTOCOL,
|
|
86
87
|
async listObjects(prefix) {
|
|
87
88
|
const objects = [];
|
|
88
|
-
for await (const blob of container.listBlobsFlat({ prefix: getListPrefix(prefix) })) {
|
|
89
|
+
for await (const blob of container().listBlobsFlat({ prefix: getListPrefix(prefix) })) {
|
|
89
90
|
const storageUri = getListedStorageUri(blob.name);
|
|
90
91
|
if (storageUri === null) continue;
|
|
91
92
|
objects.push({
|
|
@@ -102,17 +103,36 @@ function azureBlobStorage(config) {
|
|
|
102
103
|
for (let offset = 0; offset < keys.length; offset += 256) {
|
|
103
104
|
const batch = keys.slice(offset, offset + 256);
|
|
104
105
|
if (batch.length === 0) continue;
|
|
105
|
-
const failed = (await container.getBlobBatchClient().deleteBlobs(batch.map((key) => container.getBlobClient(key)))).subResponses.filter(({ status }) => status >= 300 && status !== 404);
|
|
106
|
+
const failed = (await container().getBlobBatchClient().deleteBlobs(batch.map((key) => container().getBlobClient(key)))).subResponses.filter(({ status }) => status >= 300 && status !== 404);
|
|
106
107
|
if (failed.length > 0) throw new Error(`Failed to delete ${failed.length} Azure blob(s).`);
|
|
107
108
|
}
|
|
108
109
|
},
|
|
109
|
-
async put({ key, body, contentType }) {
|
|
110
|
+
async put({ key, body, contentLength, contentType }) {
|
|
110
111
|
const storageKey = getStorageKey(key);
|
|
111
|
-
const
|
|
112
|
-
await container.getBlockBlobClient(storageKey).uploadData(bytes, { blobHTTPHeaders: {
|
|
112
|
+
const options = { blobHTTPHeaders: {
|
|
113
113
|
blobContentType: contentType,
|
|
114
114
|
blobCacheControl: "max-age=31536000"
|
|
115
|
-
} }
|
|
115
|
+
} };
|
|
116
|
+
const blob = container().getBlockBlobClient(storageKey);
|
|
117
|
+
if (contentLength === void 0) await blob.uploadData(new Uint8Array(await new Response(body).arrayBuffer()), options);
|
|
118
|
+
else {
|
|
119
|
+
const { Readable } = await import("node:stream");
|
|
120
|
+
const source = Readable.fromWeb(body);
|
|
121
|
+
const aborted = new AbortController();
|
|
122
|
+
let failure;
|
|
123
|
+
source.once("error", (error) => {
|
|
124
|
+
failure = error;
|
|
125
|
+
aborted.abort(error);
|
|
126
|
+
});
|
|
127
|
+
try {
|
|
128
|
+
await blob.upload(source, contentLength, {
|
|
129
|
+
...options,
|
|
130
|
+
abortSignal: aborted.signal
|
|
131
|
+
});
|
|
132
|
+
} catch (error) {
|
|
133
|
+
throw failure ?? error;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
116
136
|
return { storageUri: createStorageUri({
|
|
117
137
|
bucket: containerName,
|
|
118
138
|
key: storageKey,
|
|
@@ -122,7 +142,7 @@ function azureBlobStorage(config) {
|
|
|
122
142
|
async get({ storageUri }) {
|
|
123
143
|
const { key } = parseAndValidate(storageUri);
|
|
124
144
|
try {
|
|
125
|
-
const download = await container.getBlobClient(key).download();
|
|
145
|
+
const download = await container().getBlobClient(key).download();
|
|
126
146
|
const body = download.readableStreamBody;
|
|
127
147
|
if (body === void 0) return { response: null };
|
|
128
148
|
const headers = new Headers();
|
|
@@ -142,11 +162,11 @@ function azureBlobStorage(config) {
|
|
|
142
162
|
},
|
|
143
163
|
async exists({ storageUri }) {
|
|
144
164
|
const { key } = parseAndValidate(storageUri);
|
|
145
|
-
return { exists: await container.getBlobClient(key).exists() };
|
|
165
|
+
return { exists: await container().getBlobClient(key).exists() };
|
|
146
166
|
},
|
|
147
167
|
async delete({ storageUri }) {
|
|
148
168
|
const { key } = parseAndValidate(storageUri);
|
|
149
|
-
await container.getBlobClient(key).deleteIfExists();
|
|
169
|
+
await container().getBlobClient(key).deleteIfExists();
|
|
150
170
|
return { deleted: true };
|
|
151
171
|
}
|
|
152
172
|
});
|
|
@@ -363,7 +383,15 @@ const emulatorAborted = (error) => statusOf(error) === 500 && /PostgresError\(Sq
|
|
|
363
383
|
* replaces the row it read.
|
|
364
384
|
*/
|
|
365
385
|
const createCosmosStore = ({ client, databaseId, containerId, engine, nativePageSize }) => {
|
|
366
|
-
|
|
386
|
+
let opened;
|
|
387
|
+
const open = () => opened ??= (() => {
|
|
388
|
+
const account = typeof client === "function" ? client() : client;
|
|
389
|
+
return {
|
|
390
|
+
client: account,
|
|
391
|
+
container: account.database(databaseId).container(containerId)
|
|
392
|
+
};
|
|
393
|
+
})();
|
|
394
|
+
const container = () => open().container;
|
|
367
395
|
/**
|
|
368
396
|
* The state of the items `ids` names, by id, with the rows of `rowIds`;
|
|
369
397
|
* a missing item has none.
|
|
@@ -371,7 +399,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
|
|
|
371
399
|
const readStates = async (ids, rowIds) => {
|
|
372
400
|
const states = /* @__PURE__ */ new Map();
|
|
373
401
|
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) {
|
|
374
|
-
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);
|
|
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);
|
|
375
403
|
for (const { id, etag, v, expireAt, row } of resources) states.set(id, {
|
|
376
404
|
etag,
|
|
377
405
|
v,
|
|
@@ -509,7 +537,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
|
|
|
509
537
|
const found = /* @__PURE__ */ new Map();
|
|
510
538
|
const ids = [...new Set(keys.map(cosmosItemId))];
|
|
511
539
|
for (let at = 0; at < ids.length; at += GET_BATCH) {
|
|
512
|
-
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);
|
|
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);
|
|
513
541
|
for (const resource of resources) found.set(resource.id, toRow(resource.row));
|
|
514
542
|
}
|
|
515
543
|
return keys.map((key) => found.get(cosmosItemId(key)) ?? null);
|
|
@@ -545,7 +573,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
|
|
|
545
573
|
value: after
|
|
546
574
|
});
|
|
547
575
|
}
|
|
548
|
-
const { resources } = await container.items.query({
|
|
576
|
+
const { resources } = await container().items.query({
|
|
549
577
|
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"}`,
|
|
550
578
|
parameters
|
|
551
579
|
}, {
|
|
@@ -595,7 +623,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
|
|
|
595
623
|
if (planned.length > BATCH_OPERATIONS) throw new Error(`A Cosmos DB batch takes ${BATCH_OPERATIONS} operations; this write needs ${planned.length}.`);
|
|
596
624
|
let results;
|
|
597
625
|
try {
|
|
598
|
-
const response = await container.items.batch(planned.map(({ operation }) => operation), COSMOS_PARTITION);
|
|
626
|
+
const response = await container().items.batch(planned.map(({ operation }) => operation), COSMOS_PARTITION);
|
|
599
627
|
if (response.result === void 0) throw Object.assign(/* @__PURE__ */ new Error(`Cosmos DB refused the batch with status ${response.code}`), { code: response.code });
|
|
600
628
|
results = response.result;
|
|
601
629
|
} catch (error) {
|
|
@@ -629,7 +657,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
|
|
|
629
657
|
async deleteConsumed(keys) {
|
|
630
658
|
let remaining = [...new Set(keys.map(cosmosItemId))];
|
|
631
659
|
for (let attempt = 0; remaining.length > 0; attempt += 1) {
|
|
632
|
-
const failed = (await container.items.executeBulkOperations(remaining.map((id) => ({
|
|
660
|
+
const failed = (await container().items.executeBulkOperations(remaining.map((id) => ({
|
|
633
661
|
operationType: "Delete",
|
|
634
662
|
id,
|
|
635
663
|
partitionKey: COSMOS_PARTITION
|
|
@@ -650,8 +678,8 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
|
|
|
650
678
|
migrations: {
|
|
651
679
|
/** Creates the database and the container when they are missing. */
|
|
652
680
|
async apply() {
|
|
653
|
-
if (engine !== void 0) await assertCosmosContainerReusable(container, engine);
|
|
654
|
-
const { database } = await client.databases.createIfNotExists({ id: databaseId });
|
|
681
|
+
if (engine !== void 0) await assertCosmosContainerReusable(container(), engine);
|
|
682
|
+
const { database } = await open().client.databases.createIfNotExists({ id: databaseId });
|
|
655
683
|
const { resource } = await database.containers.createIfNotExists({
|
|
656
684
|
id: containerId,
|
|
657
685
|
partitionKey: { paths: [COSMOS_PARTITION_KEY_PATH] },
|
|
@@ -687,15 +715,15 @@ async apply() {
|
|
|
687
715
|
const adapterOf = (config) => {
|
|
688
716
|
const { databaseId = AZURE_COSMOS_V1_DATABASE, containerId = AZURE_COSMOS_V1_CONTAINER, aggregateBatching: _, ...connection } = config;
|
|
689
717
|
if (connection.consistencyLevel !== void 0 && connection.consistencyLevel !== "Strong") throw new Error(`cosmosDatabase reads with strong consistency; consistencyLevel "${String(connection.consistencyLevel)}" is weaker.`);
|
|
690
|
-
|
|
718
|
+
let client;
|
|
691
719
|
const store = createCosmosStore({
|
|
692
|
-
client,
|
|
720
|
+
client: () => client ??= new CosmosClient(connection),
|
|
693
721
|
databaseId,
|
|
694
722
|
containerId,
|
|
695
723
|
engine: currentCosmosEngine()
|
|
696
724
|
});
|
|
697
725
|
return {
|
|
698
|
-
client,
|
|
726
|
+
dispose: () => client?.dispose(),
|
|
699
727
|
adapter: createKvAdapter({ store })
|
|
700
728
|
};
|
|
701
729
|
};
|
|
@@ -707,8 +735,23 @@ const adapterOf = (config) => {
|
|
|
707
735
|
* it with the managed server's plugins.
|
|
708
736
|
*/
|
|
709
737
|
const migrateCosmosDatabase = (config, plugins = []) => {
|
|
710
|
-
const {
|
|
711
|
-
return migrateCoreSchema(adapter, "cosmosDatabase", plugins).finally(
|
|
738
|
+
const { dispose, adapter } = adapterOf(config);
|
|
739
|
+
return migrateCoreSchema(adapter, "cosmosDatabase", plugins).finally(dispose);
|
|
740
|
+
};
|
|
741
|
+
/**
|
|
742
|
+
* The resource this database reaches, without credentials, under the key
|
|
743
|
+
* `hot-updater doctor` reads to tell the database `hot-updater init` saved
|
|
744
|
+
* from another (a convention CLAUDE.md documents).
|
|
745
|
+
*/
|
|
746
|
+
const DATABASE_TARGET = Symbol.for("@hot-updater/database-target");
|
|
747
|
+
/** The account host a client reaches: its endpoint, or the connection string's. */
|
|
748
|
+
const accountHostOf = ({ endpoint, connectionString }) => {
|
|
749
|
+
const url = endpoint ?? connectionString?.match(/AccountEndpoint=([^;]+)/iu)?.[1];
|
|
750
|
+
try {
|
|
751
|
+
return url === void 0 ? void 0 : new URL(url).hostname;
|
|
752
|
+
} catch {
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
712
755
|
};
|
|
713
756
|
/**
|
|
714
757
|
* Hot Updater's database in one Azure Cosmos DB for NoSQL container,
|
|
@@ -717,16 +760,18 @@ const migrateCosmosDatabase = (config, plugins = []) => {
|
|
|
717
760
|
* 20 GB.
|
|
718
761
|
*/
|
|
719
762
|
const cosmosDatabase = (config) => {
|
|
720
|
-
const {
|
|
763
|
+
const { dispose, adapter } = adapterOf(config);
|
|
764
|
+
const host = accountHostOf(config);
|
|
721
765
|
return {
|
|
722
766
|
...createEngineDatabase({
|
|
723
767
|
name: "cosmosDatabase",
|
|
724
768
|
adapter,
|
|
725
769
|
aggregateBatching: config.aggregateBatching ?? {}
|
|
726
770
|
}),
|
|
771
|
+
...host === void 0 ? {} : { [DATABASE_TARGET]: `cosmos/${host}` },
|
|
727
772
|
async dispose() {
|
|
728
773
|
await adapter.dispose?.();
|
|
729
|
-
|
|
774
|
+
dispose();
|
|
730
775
|
}
|
|
731
776
|
};
|
|
732
777
|
};
|
package/dist/init/index.cjs
CHANGED
|
@@ -266,7 +266,15 @@ const emulatorAborted = (error) => statusOf(error) === 500 && /PostgresError\(Sq
|
|
|
266
266
|
* replaces the row it read.
|
|
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;
|
|
272
|
+
return {
|
|
273
|
+
client: account,
|
|
274
|
+
container: account.database(databaseId).container(containerId)
|
|
275
|
+
};
|
|
276
|
+
})();
|
|
277
|
+
const container = () => open().container;
|
|
270
278
|
/**
|
|
271
279
|
* The state of the items `ids` names, by id, with the rows of `rowIds`;
|
|
272
280
|
* a missing item has none.
|
|
@@ -274,7 +282,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
|
|
|
274
282
|
const readStates = async (ids, rowIds) => {
|
|
275
283
|
const states = /* @__PURE__ */ new Map();
|
|
276
284
|
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) {
|
|
277
|
-
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);
|
|
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);
|
|
278
286
|
for (const { id, etag, v, expireAt, row } of resources) states.set(id, {
|
|
279
287
|
etag,
|
|
280
288
|
v,
|
|
@@ -412,7 +420,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
|
|
|
412
420
|
const found = /* @__PURE__ */ new Map();
|
|
413
421
|
const ids = [...new Set(keys.map(cosmosItemId))];
|
|
414
422
|
for (let at = 0; at < ids.length; at += GET_BATCH) {
|
|
415
|
-
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);
|
|
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);
|
|
416
424
|
for (const resource of resources) found.set(resource.id, toRow(resource.row));
|
|
417
425
|
}
|
|
418
426
|
return keys.map((key) => found.get(cosmosItemId(key)) ?? null);
|
|
@@ -448,7 +456,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
|
|
|
448
456
|
value: after
|
|
449
457
|
});
|
|
450
458
|
}
|
|
451
|
-
const { resources } = await container.items.query({
|
|
459
|
+
const { resources } = await container().items.query({
|
|
452
460
|
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"}`,
|
|
453
461
|
parameters
|
|
454
462
|
}, {
|
|
@@ -498,7 +506,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
|
|
|
498
506
|
if (planned.length > BATCH_OPERATIONS) throw new Error(`A Cosmos DB batch takes ${BATCH_OPERATIONS} operations; this write needs ${planned.length}.`);
|
|
499
507
|
let results;
|
|
500
508
|
try {
|
|
501
|
-
const response = await container.items.batch(planned.map(({ operation }) => operation), COSMOS_PARTITION);
|
|
509
|
+
const response = await container().items.batch(planned.map(({ operation }) => operation), COSMOS_PARTITION);
|
|
502
510
|
if (response.result === void 0) throw Object.assign(/* @__PURE__ */ new Error(`Cosmos DB refused the batch with status ${response.code}`), { code: response.code });
|
|
503
511
|
results = response.result;
|
|
504
512
|
} catch (error) {
|
|
@@ -532,7 +540,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
|
|
|
532
540
|
async deleteConsumed(keys) {
|
|
533
541
|
let remaining = [...new Set(keys.map(cosmosItemId))];
|
|
534
542
|
for (let attempt = 0; remaining.length > 0; attempt += 1) {
|
|
535
|
-
const failed = (await container.items.executeBulkOperations(remaining.map((id) => ({
|
|
543
|
+
const failed = (await container().items.executeBulkOperations(remaining.map((id) => ({
|
|
536
544
|
operationType: "Delete",
|
|
537
545
|
id,
|
|
538
546
|
partitionKey: COSMOS_PARTITION
|
|
@@ -553,8 +561,8 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
|
|
|
553
561
|
migrations: {
|
|
554
562
|
/** Creates the database and the container when they are missing. */
|
|
555
563
|
async apply() {
|
|
556
|
-
if (engine !== void 0) await assertCosmosContainerReusable(container, engine);
|
|
557
|
-
const { database } = await client.databases.createIfNotExists({ id: databaseId });
|
|
564
|
+
if (engine !== void 0) await assertCosmosContainerReusable(container(), engine);
|
|
565
|
+
const { database } = await open().client.databases.createIfNotExists({ id: databaseId });
|
|
558
566
|
const { resource } = await database.containers.createIfNotExists({
|
|
559
567
|
id: containerId,
|
|
560
568
|
partitionKey: { paths: [COSMOS_PARTITION_KEY_PATH] },
|
|
@@ -590,15 +598,15 @@ async apply() {
|
|
|
590
598
|
const adapterOf = (config) => {
|
|
591
599
|
const { databaseId = AZURE_COSMOS_V1_DATABASE, containerId = AZURE_COSMOS_V1_CONTAINER, aggregateBatching: _, ...connection } = config;
|
|
592
600
|
if (connection.consistencyLevel !== void 0 && connection.consistencyLevel !== "Strong") throw new Error(`cosmosDatabase reads with strong consistency; consistencyLevel "${String(connection.consistencyLevel)}" is weaker.`);
|
|
593
|
-
|
|
601
|
+
let client;
|
|
594
602
|
const store = createCosmosStore({
|
|
595
|
-
client,
|
|
603
|
+
client: () => client ??= new _azure_cosmos.CosmosClient(connection),
|
|
596
604
|
databaseId,
|
|
597
605
|
containerId,
|
|
598
606
|
engine: currentCosmosEngine()
|
|
599
607
|
});
|
|
600
608
|
return {
|
|
601
|
-
client,
|
|
609
|
+
dispose: () => client?.dispose(),
|
|
602
610
|
adapter: (0, _hot_updater_plugin_core.createKvAdapter)({ store })
|
|
603
611
|
};
|
|
604
612
|
};
|
|
@@ -610,8 +618,23 @@ const adapterOf = (config) => {
|
|
|
610
618
|
* it with the managed server's plugins.
|
|
611
619
|
*/
|
|
612
620
|
const migrateCosmosDatabase = (config, plugins = []) => {
|
|
613
|
-
const {
|
|
614
|
-
return (0, _hot_updater_plugin_core.migrateCoreSchema)(adapter, "cosmosDatabase", plugins).finally(
|
|
621
|
+
const { dispose, adapter } = adapterOf(config);
|
|
622
|
+
return (0, _hot_updater_plugin_core.migrateCoreSchema)(adapter, "cosmosDatabase", plugins).finally(dispose);
|
|
623
|
+
};
|
|
624
|
+
/**
|
|
625
|
+
* The resource this database reaches, without credentials, under the key
|
|
626
|
+
* `hot-updater doctor` reads to tell the database `hot-updater init` saved
|
|
627
|
+
* from another (a convention CLAUDE.md documents).
|
|
628
|
+
*/
|
|
629
|
+
const DATABASE_TARGET = Symbol.for("@hot-updater/database-target");
|
|
630
|
+
/** The account host a client reaches: its endpoint, or the connection string's. */
|
|
631
|
+
const accountHostOf = ({ endpoint, connectionString }) => {
|
|
632
|
+
const url = endpoint ?? connectionString?.match(/AccountEndpoint=([^;]+)/iu)?.[1];
|
|
633
|
+
try {
|
|
634
|
+
return url === void 0 ? void 0 : new URL(url).hostname;
|
|
635
|
+
} catch {
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
615
638
|
};
|
|
616
639
|
/**
|
|
617
640
|
* Hot Updater's database in one Azure Cosmos DB for NoSQL container,
|
|
@@ -620,16 +643,18 @@ const migrateCosmosDatabase = (config, plugins = []) => {
|
|
|
620
643
|
* 20 GB.
|
|
621
644
|
*/
|
|
622
645
|
const cosmosDatabase = (config) => {
|
|
623
|
-
const {
|
|
646
|
+
const { dispose, adapter } = adapterOf(config);
|
|
647
|
+
const host = accountHostOf(config);
|
|
624
648
|
return {
|
|
625
649
|
...(0, _hot_updater_plugin_core.createEngineDatabase)({
|
|
626
650
|
name: "cosmosDatabase",
|
|
627
651
|
adapter,
|
|
628
652
|
aggregateBatching: config.aggregateBatching ?? {}
|
|
629
653
|
}),
|
|
654
|
+
...host === void 0 ? {} : { [DATABASE_TARGET]: `cosmos/${host}` },
|
|
630
655
|
async dispose() {
|
|
631
656
|
await adapter.dispose?.();
|
|
632
|
-
|
|
657
|
+
dispose();
|
|
633
658
|
}
|
|
634
659
|
};
|
|
635
660
|
};
|