@tinycloud/sdk-services 2.3.0 → 2.4.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +326 -52
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +325 -52
- package/dist/index.js.map +1 -1
- package/dist/kv/index.cjs +70 -29
- package/dist/kv/index.cjs.map +1 -1
- package/dist/kv/index.d.cts +28 -0
- package/dist/kv/index.d.ts +28 -0
- package/dist/kv/index.js +70 -29
- package/dist/kv/index.js.map +1 -1
- package/dist/sql/index.cjs +247 -16
- package/dist/sql/index.cjs.map +1 -1
- package/dist/sql/index.d.cts +70 -2
- package/dist/sql/index.d.ts +70 -2
- package/dist/sql/index.js +246 -16
- package/dist/sql/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -3,7 +3,7 @@ export { g as ErrorCode, h as ErrorCodes, i as FetchRequestInit, j as FetchRespo
|
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { IKVService } from './kv/index.cjs';
|
|
5
5
|
export { DEFAULT_SIGNED_READ_URL_EXPIRY_MS, IPrefixedKVService, KVAction, KVActionType, KVBatchPutItem, KVBatchPutOptions, KVBatchPutResponse, KVCreateSignedReadUrlOptions, KVDeleteOptions, KVGetOptions, KVHeadOptions, KVListOptions, KVListResponse, KVPutOptions, KVResponse, KVResponseHeaders, KVService, KVServiceConfig, KVSignedReadUrlResponse, PrefixedKVService } from './kv/index.cjs';
|
|
6
|
-
export { BatchOptions, BatchResponse, DatabaseHandle, ExecuteOptions, ExecuteResponse, IDatabaseHandle, ISQLService, QueryOptions, QueryResponse, SQLAction, SQLActionType, SQLService, SQLServiceConfig, SqlStatement, SqlValue } from './sql/index.cjs';
|
|
6
|
+
export { BatchOptions, BatchResponse, DatabaseHandle, ExecuteOptions, ExecuteResponse, IDatabaseHandle, ISQLMigrations, ISQLService, QueryOptions, QueryResponse, SQLAction, SQLActionType, SQLMigrations, SQLService, SQLServiceConfig, SqlMigration, SqlMigrationApplyOptions, SqlMigrationApplyResponse, SqlStatement, SqlValue } from './sql/index.cjs';
|
|
7
7
|
import { IEncryptionService, DecryptCapabilityProof } from './encryption/index.cjs';
|
|
8
8
|
export { BuildCanonicalDecryptRequestInput, BuildDecryptFactsInput, BuildDecryptInvocationInput, BuiltDecryptInvocation, CanonicalDecryptRequest, CanonicalJson, DECRYPT_ACTION, DECRYPT_FACT_TYPE, DECRYPT_RESULT_TYPE, DEFAULT_ENCRYPTION_ALG, DEFAULT_KEY_VERSION, DecryptEnvelopeOptions, DecryptInvocationFact, DecryptInvocationSigner, DecryptRequestBody, DecryptResponseBody, DecryptTransport, DiscoverNetworkInput, DiscoveredNetwork, DiscoverySource, ENCRYPTION_NETWORK_URN_PREFIX, ENCRYPTION_SERVICE, ENCRYPTION_SERVICE_SHORT, ENVELOPE_VERSION, EncryptToNetworkInput, EncryptToNetworkOptions, EncryptToNetworkResult, EncryptionCrypto, EncryptionError, EncryptionErrorInput, EncryptionService, EncryptionServiceConfig, InlineEncryptedEnvelope, Json, NETWORK_NAME_PATTERN, NetworkDescriptor, NetworkIdError, NodeDescriptorFetcher, ParsedNetworkId, RandomReceiverKeyInput, ReceiverKeyPair, ReceiverKeySigner, SignedReceiverKeyInput, VerifyDecryptResponseInput, WellKnownDescriptorFetcher, base64Decode, base64Encode, buildCanonicalDecryptRequest, buildDecryptAttenuation, buildDecryptFacts, buildDecryptInvocation, buildNetworkId, canonicalHashHex, canonicalSignedResponse, canonicalize as canonicalizeEncryptionJson, checkDecryptInvocationInput, decryptEnvelopeWithKey, deriveSignedReceiverKey, discoverNetwork, encryptToNetwork, encryptionError, ensureNetworkUsableForDecrypt, generateRandomReceiverKey, hexDecode, hexEncode, isNetworkId, networkDiscoveryKey, openWrappedKey, parseNetworkId, utf8Decode, utf8Encode, validateEnvelope, verifyDecryptResponse } from './encryption/index.cjs';
|
|
9
9
|
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { g as ErrorCode, h as ErrorCodes, i as FetchRequestInit, j as FetchRespo
|
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { IKVService } from './kv/index.js';
|
|
5
5
|
export { DEFAULT_SIGNED_READ_URL_EXPIRY_MS, IPrefixedKVService, KVAction, KVActionType, KVBatchPutItem, KVBatchPutOptions, KVBatchPutResponse, KVCreateSignedReadUrlOptions, KVDeleteOptions, KVGetOptions, KVHeadOptions, KVListOptions, KVListResponse, KVPutOptions, KVResponse, KVResponseHeaders, KVService, KVServiceConfig, KVSignedReadUrlResponse, PrefixedKVService } from './kv/index.js';
|
|
6
|
-
export { BatchOptions, BatchResponse, DatabaseHandle, ExecuteOptions, ExecuteResponse, IDatabaseHandle, ISQLService, QueryOptions, QueryResponse, SQLAction, SQLActionType, SQLService, SQLServiceConfig, SqlStatement, SqlValue } from './sql/index.js';
|
|
6
|
+
export { BatchOptions, BatchResponse, DatabaseHandle, ExecuteOptions, ExecuteResponse, IDatabaseHandle, ISQLMigrations, ISQLService, QueryOptions, QueryResponse, SQLAction, SQLActionType, SQLMigrations, SQLService, SQLServiceConfig, SqlMigration, SqlMigrationApplyOptions, SqlMigrationApplyResponse, SqlStatement, SqlValue } from './sql/index.js';
|
|
7
7
|
import { IEncryptionService, DecryptCapabilityProof } from './encryption/index.js';
|
|
8
8
|
export { BuildCanonicalDecryptRequestInput, BuildDecryptFactsInput, BuildDecryptInvocationInput, BuiltDecryptInvocation, CanonicalDecryptRequest, CanonicalJson, DECRYPT_ACTION, DECRYPT_FACT_TYPE, DECRYPT_RESULT_TYPE, DEFAULT_ENCRYPTION_ALG, DEFAULT_KEY_VERSION, DecryptEnvelopeOptions, DecryptInvocationFact, DecryptInvocationSigner, DecryptRequestBody, DecryptResponseBody, DecryptTransport, DiscoverNetworkInput, DiscoveredNetwork, DiscoverySource, ENCRYPTION_NETWORK_URN_PREFIX, ENCRYPTION_SERVICE, ENCRYPTION_SERVICE_SHORT, ENVELOPE_VERSION, EncryptToNetworkInput, EncryptToNetworkOptions, EncryptToNetworkResult, EncryptionCrypto, EncryptionError, EncryptionErrorInput, EncryptionService, EncryptionServiceConfig, InlineEncryptedEnvelope, Json, NETWORK_NAME_PATTERN, NetworkDescriptor, NetworkIdError, NodeDescriptorFetcher, ParsedNetworkId, RandomReceiverKeyInput, ReceiverKeyPair, ReceiverKeySigner, SignedReceiverKeyInput, VerifyDecryptResponseInput, WellKnownDescriptorFetcher, base64Decode, base64Encode, buildCanonicalDecryptRequest, buildDecryptAttenuation, buildDecryptFacts, buildDecryptInvocation, buildNetworkId, canonicalHashHex, canonicalSignedResponse, canonicalize as canonicalizeEncryptionJson, checkDecryptInvocationInput, decryptEnvelopeWithKey, deriveSignedReceiverKey, discoverNetwork, encryptToNetwork, encryptionError, ensureNetworkUsableForDecrypt, generateRandomReceiverKey, hexDecode, hexEncode, isNetworkId, networkDiscoveryKey, openWrappedKey, parseNetworkId, utf8Decode, utf8Encode, validateEnvelope, verifyDecryptResponse } from './encryption/index.js';
|
|
9
9
|
|
package/dist/index.js
CHANGED
|
@@ -983,6 +983,31 @@ var KVService = class extends BaseService {
|
|
|
983
983
|
}
|
|
984
984
|
return void 0;
|
|
985
985
|
}
|
|
986
|
+
/**
|
|
987
|
+
* Classify a KV 404 by reading the response body once.
|
|
988
|
+
*
|
|
989
|
+
* The server returns 404 both for a genuinely missing key AND for an
|
|
990
|
+
* un-hosted space (body "Space not found"). Previously get/head/delete
|
|
991
|
+
* collapsed every 404 to KV_NOT_FOUND before reading the body, so an
|
|
992
|
+
* un-hosted-space read was indistinguishable from a missing key. We now
|
|
993
|
+
* preserve status + the "Space not found" body for the un-hosted case (so the
|
|
994
|
+
* CLI/SDK can normalize it to SPACE_NOT_HOSTED, matching put/list/sql), and
|
|
995
|
+
* fall through to KV_NOT_FOUND for a real missing key.
|
|
996
|
+
*/
|
|
997
|
+
async classifyNotFound(response, key) {
|
|
998
|
+
const errorText = await response.text();
|
|
999
|
+
if (/space not found/i.test(errorText)) {
|
|
1000
|
+
return err(
|
|
1001
|
+
serviceError(
|
|
1002
|
+
ErrorCodes.KV_NOT_FOUND,
|
|
1003
|
+
`KV ${response.status} - ${errorText}`,
|
|
1004
|
+
"kv",
|
|
1005
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
1006
|
+
)
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
return err(serviceError(ErrorCodes.KV_NOT_FOUND, `Key not found: ${key}`, "kv"));
|
|
1010
|
+
}
|
|
986
1011
|
/**
|
|
987
1012
|
* Get the full path with optional prefix.
|
|
988
1013
|
*
|
|
@@ -1033,6 +1058,38 @@ var KVService = class extends BaseService {
|
|
|
1033
1058
|
signal: this.combineSignals(signal)
|
|
1034
1059
|
});
|
|
1035
1060
|
}
|
|
1061
|
+
/**
|
|
1062
|
+
* Serialize a single put value into a fetch body.
|
|
1063
|
+
*
|
|
1064
|
+
* Binary values (Blob/ArrayBuffer/typed-array, incl. Node Buffer) are sent as
|
|
1065
|
+
* raw bytes (as a Blob) so they round-trip byte-identically — without this a
|
|
1066
|
+
* Buffer would be JSON.stringify'd into `{"type":"Buffer","data":[...]}`.
|
|
1067
|
+
* Strings are returned unchanged (preserving prior behavior); other values are
|
|
1068
|
+
* JSON-encoded. `contentType` overrides the inferred type for binary values.
|
|
1069
|
+
*/
|
|
1070
|
+
serializePutValue(value, contentType) {
|
|
1071
|
+
if (value instanceof Blob) {
|
|
1072
|
+
if (!contentType || value.type === contentType) {
|
|
1073
|
+
return value;
|
|
1074
|
+
}
|
|
1075
|
+
return new Blob([value], { type: contentType });
|
|
1076
|
+
}
|
|
1077
|
+
if (value instanceof ArrayBuffer) {
|
|
1078
|
+
return new Blob([value], {
|
|
1079
|
+
type: contentType ?? "application/octet-stream"
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
if (ArrayBuffer.isView(value)) {
|
|
1083
|
+
const view = new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
1084
|
+
return new Blob([view], {
|
|
1085
|
+
type: contentType ?? "application/octet-stream"
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
if (typeof value === "string") {
|
|
1089
|
+
return contentType ? new Blob([value], { type: contentType }) : value;
|
|
1090
|
+
}
|
|
1091
|
+
return JSON.stringify(value);
|
|
1092
|
+
}
|
|
1036
1093
|
serializeBatchPutValue(item) {
|
|
1037
1094
|
const contentType = item.contentType;
|
|
1038
1095
|
if (item.value instanceof Blob) {
|
|
@@ -1102,10 +1159,13 @@ var KVService = class extends BaseService {
|
|
|
1102
1159
|
* @param raw - Whether to return raw text
|
|
1103
1160
|
* @returns Parsed data
|
|
1104
1161
|
*/
|
|
1105
|
-
async parseResponse(response, raw = false) {
|
|
1162
|
+
async parseResponse(response, raw = false, binary = false) {
|
|
1106
1163
|
if (!response.ok) {
|
|
1107
1164
|
return void 0;
|
|
1108
1165
|
}
|
|
1166
|
+
if (binary) {
|
|
1167
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
1168
|
+
}
|
|
1109
1169
|
if (raw) {
|
|
1110
1170
|
return await response.text();
|
|
1111
1171
|
}
|
|
@@ -1194,13 +1254,7 @@ var KVService = class extends BaseService {
|
|
|
1194
1254
|
}));
|
|
1195
1255
|
}
|
|
1196
1256
|
if (response.status === 404) {
|
|
1197
|
-
return
|
|
1198
|
-
serviceError(
|
|
1199
|
-
ErrorCodes.KV_NOT_FOUND,
|
|
1200
|
-
`Key not found: ${key}`,
|
|
1201
|
-
"kv"
|
|
1202
|
-
)
|
|
1203
|
-
);
|
|
1257
|
+
return this.classifyNotFound(response, key);
|
|
1204
1258
|
}
|
|
1205
1259
|
const errorText = await response.text();
|
|
1206
1260
|
return err(
|
|
@@ -1212,7 +1266,11 @@ var KVService = class extends BaseService {
|
|
|
1212
1266
|
)
|
|
1213
1267
|
);
|
|
1214
1268
|
}
|
|
1215
|
-
const data = await this.parseResponse(
|
|
1269
|
+
const data = await this.parseResponse(
|
|
1270
|
+
response,
|
|
1271
|
+
options?.raw,
|
|
1272
|
+
options?.binary
|
|
1273
|
+
);
|
|
1216
1274
|
return ok({
|
|
1217
1275
|
data,
|
|
1218
1276
|
headers: this.createResponseHeaders(response.headers)
|
|
@@ -1231,12 +1289,7 @@ var KVService = class extends BaseService {
|
|
|
1231
1289
|
return err(authRequiredError("kv"));
|
|
1232
1290
|
}
|
|
1233
1291
|
const path = this.getFullPath(key, options?.prefix);
|
|
1234
|
-
|
|
1235
|
-
if (typeof value === "string") {
|
|
1236
|
-
body = value;
|
|
1237
|
-
} else {
|
|
1238
|
-
body = JSON.stringify(value);
|
|
1239
|
-
}
|
|
1292
|
+
const body = this.serializePutValue(value, options?.contentType);
|
|
1240
1293
|
try {
|
|
1241
1294
|
const response = await this.invokeOperation(
|
|
1242
1295
|
path,
|
|
@@ -1462,13 +1515,7 @@ var KVService = class extends BaseService {
|
|
|
1462
1515
|
}));
|
|
1463
1516
|
}
|
|
1464
1517
|
if (response.status === 404) {
|
|
1465
|
-
return
|
|
1466
|
-
serviceError(
|
|
1467
|
-
ErrorCodes.KV_NOT_FOUND,
|
|
1468
|
-
`Key not found: ${key}`,
|
|
1469
|
-
"kv"
|
|
1470
|
-
)
|
|
1471
|
-
);
|
|
1518
|
+
return this.classifyNotFound(response, key);
|
|
1472
1519
|
}
|
|
1473
1520
|
const errorText = await response.text();
|
|
1474
1521
|
return err(
|
|
@@ -1513,13 +1560,7 @@ var KVService = class extends BaseService {
|
|
|
1513
1560
|
}));
|
|
1514
1561
|
}
|
|
1515
1562
|
if (response.status === 404) {
|
|
1516
|
-
return
|
|
1517
|
-
serviceError(
|
|
1518
|
-
ErrorCodes.KV_NOT_FOUND,
|
|
1519
|
-
`Key not found: ${key}`,
|
|
1520
|
-
"kv"
|
|
1521
|
-
)
|
|
1522
|
-
);
|
|
1563
|
+
return this.classifyNotFound(response, key);
|
|
1523
1564
|
}
|
|
1524
1565
|
const errorText = await response.text();
|
|
1525
1566
|
return err(
|
|
@@ -1646,11 +1687,59 @@ var KVService = class extends BaseService {
|
|
|
1646
1687
|
*/
|
|
1647
1688
|
KVService.serviceName = "kv";
|
|
1648
1689
|
|
|
1690
|
+
// src/responseErrors.ts
|
|
1691
|
+
function parseServiceErrorBody(errorText) {
|
|
1692
|
+
try {
|
|
1693
|
+
return JSON.parse(errorText);
|
|
1694
|
+
} catch {
|
|
1695
|
+
return {};
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
function formatServiceResponseError(serviceLabel, operation, status, errorText, parsed) {
|
|
1699
|
+
if (parsed.message) {
|
|
1700
|
+
return compactMessage(parsed.message);
|
|
1701
|
+
}
|
|
1702
|
+
if (looksLikeHtml(errorText)) {
|
|
1703
|
+
if (status === 524 || /524\s*[:-]/i.test(errorText) || /a timeout occurred/i.test(errorText)) {
|
|
1704
|
+
return `${serviceLabel} ${operation} failed: upstream request timed out. Please retry.`;
|
|
1705
|
+
}
|
|
1706
|
+
return `${serviceLabel} ${operation} failed: upstream service returned an HTML error page (${status}).`;
|
|
1707
|
+
}
|
|
1708
|
+
return `${serviceLabel} ${operation} failed: ${status} - ${compactMessage(errorText)}`;
|
|
1709
|
+
}
|
|
1710
|
+
function responseErrorMeta(status, statusText, errorText) {
|
|
1711
|
+
const meta = { status, statusText };
|
|
1712
|
+
const snippet = compactMessage(errorText);
|
|
1713
|
+
if (snippet) {
|
|
1714
|
+
meta.responseSnippet = snippet.slice(0, 300);
|
|
1715
|
+
}
|
|
1716
|
+
return meta;
|
|
1717
|
+
}
|
|
1718
|
+
function looksLikeHtml(text) {
|
|
1719
|
+
return /<!doctype html/i.test(text) || /<html[\s>]/i.test(text);
|
|
1720
|
+
}
|
|
1721
|
+
function compactMessage(message) {
|
|
1722
|
+
const compact = message.replace(/\s+/g, " ").trim();
|
|
1723
|
+
return compact.length > 500 ? `${compact.slice(0, 500)}...` : compact;
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
// src/sql/SQLMigrations.ts
|
|
1727
|
+
var SQLMigrations = class {
|
|
1728
|
+
constructor(service, dbName) {
|
|
1729
|
+
this.service = service;
|
|
1730
|
+
this.dbName = dbName;
|
|
1731
|
+
}
|
|
1732
|
+
apply(options) {
|
|
1733
|
+
return this.service.applyMigrationsOnDb(this.dbName, options);
|
|
1734
|
+
}
|
|
1735
|
+
};
|
|
1736
|
+
|
|
1649
1737
|
// src/sql/DatabaseHandle.ts
|
|
1650
1738
|
var DatabaseHandle = class {
|
|
1651
1739
|
constructor(service, name) {
|
|
1652
1740
|
this.service = service;
|
|
1653
1741
|
this.name = name;
|
|
1742
|
+
this.migrations = new SQLMigrations(service, name);
|
|
1654
1743
|
}
|
|
1655
1744
|
async query(sql, params, options) {
|
|
1656
1745
|
return this.service.queryOnDb(this.name, sql, params, options);
|
|
@@ -1673,6 +1762,7 @@ var DatabaseHandle = class {
|
|
|
1673
1762
|
var SQLAction = {
|
|
1674
1763
|
READ: "tinycloud.sql/read",
|
|
1675
1764
|
WRITE: "tinycloud.sql/write",
|
|
1765
|
+
DDL: "tinycloud.sql/ddl",
|
|
1676
1766
|
ADMIN: "tinycloud.sql/admin",
|
|
1677
1767
|
SELECT: "tinycloud.sql/select",
|
|
1678
1768
|
INSERT: "tinycloud.sql/insert",
|
|
@@ -1684,9 +1774,21 @@ var SQLAction = {
|
|
|
1684
1774
|
};
|
|
1685
1775
|
|
|
1686
1776
|
// src/sql/SQLService.ts
|
|
1777
|
+
var DDL_TOKENS = /* @__PURE__ */ new Set(["alter", "create", "drop"]);
|
|
1778
|
+
var MIGRATIONS_TABLE = "__tinycloud_sql_migrations";
|
|
1779
|
+
var MIGRATIONS_SCHEMA = `CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (
|
|
1780
|
+
key TEXT PRIMARY KEY,
|
|
1781
|
+
namespace TEXT NOT NULL,
|
|
1782
|
+
id TEXT NOT NULL,
|
|
1783
|
+
applied_at TEXT NOT NULL,
|
|
1784
|
+
statement_count INTEGER NOT NULL
|
|
1785
|
+
)`;
|
|
1786
|
+
var MIGRATIONS_META_NAMESPACE = "tinycloud.sql.migrations";
|
|
1787
|
+
var MIGRATIONS_META_ID = "000_metadata";
|
|
1687
1788
|
var SQLService = class extends BaseService {
|
|
1688
1789
|
constructor(config = {}) {
|
|
1689
1790
|
super();
|
|
1791
|
+
this.migrationLocks = /* @__PURE__ */ new Map();
|
|
1690
1792
|
this._config = config;
|
|
1691
1793
|
}
|
|
1692
1794
|
get config() {
|
|
@@ -1722,6 +1824,24 @@ var SQLService = class extends BaseService {
|
|
|
1722
1824
|
async batch(statements, options) {
|
|
1723
1825
|
return this.batchOnDb(this.defaultDbName, statements, options);
|
|
1724
1826
|
}
|
|
1827
|
+
async applyMigrationsOnDb(dbName, options) {
|
|
1828
|
+
const validationError = validateMigrationOptions(options);
|
|
1829
|
+
if (validationError) {
|
|
1830
|
+
return err(serviceError(ErrorCodes.INVALID_INPUT, validationError, "sql"));
|
|
1831
|
+
}
|
|
1832
|
+
const lockKey = `${dbName}\0${options.namespace}`;
|
|
1833
|
+
const existing = this.migrationLocks.get(lockKey);
|
|
1834
|
+
if (existing) {
|
|
1835
|
+
return existing;
|
|
1836
|
+
}
|
|
1837
|
+
const promise = this.applyMigrationsOnDbUnlocked(dbName, options);
|
|
1838
|
+
this.migrationLocks.set(lockKey, promise);
|
|
1839
|
+
try {
|
|
1840
|
+
return await promise;
|
|
1841
|
+
} finally {
|
|
1842
|
+
this.migrationLocks.delete(lockKey);
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1725
1845
|
// === Internal methods called by DatabaseHandle ===
|
|
1726
1846
|
async queryOnDb(dbName, sql, params, options) {
|
|
1727
1847
|
return this.withTelemetry("query", dbName, async () => {
|
|
@@ -1759,9 +1879,15 @@ var SQLService = class extends BaseService {
|
|
|
1759
1879
|
if (options?.schema) {
|
|
1760
1880
|
body.schema = options.schema;
|
|
1761
1881
|
}
|
|
1882
|
+
const actions = [
|
|
1883
|
+
this.actionForSql(sql, SQLAction.WRITE),
|
|
1884
|
+
...(options?.schema ?? []).map(
|
|
1885
|
+
(statement) => this.actionForSql(statement, SQLAction.DDL)
|
|
1886
|
+
)
|
|
1887
|
+
];
|
|
1762
1888
|
const response = await this.invokeSQL(
|
|
1763
1889
|
dbName,
|
|
1764
|
-
this.
|
|
1890
|
+
this.dedupeActions(actions),
|
|
1765
1891
|
body,
|
|
1766
1892
|
options?.signal
|
|
1767
1893
|
);
|
|
@@ -1783,7 +1909,7 @@ var SQLService = class extends BaseService {
|
|
|
1783
1909
|
try {
|
|
1784
1910
|
const response = await this.invokeSQL(
|
|
1785
1911
|
dbName,
|
|
1786
|
-
this.
|
|
1912
|
+
this.actionsForSqlBatch(statements),
|
|
1787
1913
|
{ action: "batch", statements },
|
|
1788
1914
|
options?.signal
|
|
1789
1915
|
);
|
|
@@ -1846,10 +1972,83 @@ var SQLService = class extends BaseService {
|
|
|
1846
1972
|
}
|
|
1847
1973
|
});
|
|
1848
1974
|
}
|
|
1975
|
+
async applyMigrationsOnDbUnlocked(dbName, options) {
|
|
1976
|
+
return this.withTelemetry("migrations.apply", dbName, async () => {
|
|
1977
|
+
const created = await this.ensureMigrationsTable(dbName, options.signal);
|
|
1978
|
+
if (!created.ok) return created;
|
|
1979
|
+
const listed = await this.queryOnDb(
|
|
1980
|
+
dbName,
|
|
1981
|
+
`SELECT id FROM ${MIGRATIONS_TABLE} WHERE namespace = ? ORDER BY applied_at, id`,
|
|
1982
|
+
[options.namespace],
|
|
1983
|
+
{ signal: options.signal }
|
|
1984
|
+
);
|
|
1985
|
+
if (!listed.ok) return listed;
|
|
1986
|
+
const appliedIds = new Set(
|
|
1987
|
+
listed.data.rows.map((row) => rowValue(row, 0)).filter((id) => typeof id === "string")
|
|
1988
|
+
);
|
|
1989
|
+
const skipped = options.migrations.filter((migration) => appliedIds.has(migration.id)).map((migration) => migration.id);
|
|
1990
|
+
const pending = options.migrations.filter((migration) => !appliedIds.has(migration.id));
|
|
1991
|
+
const applied = [];
|
|
1992
|
+
for (const migration of pending) {
|
|
1993
|
+
const result = await this.applyOneMigration(dbName, options.namespace, migration, options.signal);
|
|
1994
|
+
if (!result.ok) return result;
|
|
1995
|
+
applied.push(migration.id);
|
|
1996
|
+
}
|
|
1997
|
+
return ok({
|
|
1998
|
+
database: dbName,
|
|
1999
|
+
namespace: options.namespace,
|
|
2000
|
+
status: applied.length > 0 ? "applied" : "already_current",
|
|
2001
|
+
applied,
|
|
2002
|
+
skipped
|
|
2003
|
+
});
|
|
2004
|
+
});
|
|
2005
|
+
}
|
|
2006
|
+
async applyOneMigration(dbName, namespace, migration, signal) {
|
|
2007
|
+
const statements = [
|
|
2008
|
+
...migration.sql.map(
|
|
2009
|
+
(statement) => typeof statement === "string" ? { sql: statement } : statement
|
|
2010
|
+
),
|
|
2011
|
+
{
|
|
2012
|
+
sql: `INSERT OR REPLACE INTO ${MIGRATIONS_TABLE} (key, namespace, id, applied_at, statement_count) VALUES (?, ?, ?, ?, ?)`,
|
|
2013
|
+
params: [
|
|
2014
|
+
migrationKey(namespace, migration.id),
|
|
2015
|
+
namespace,
|
|
2016
|
+
migration.id,
|
|
2017
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
2018
|
+
migration.sql.length
|
|
2019
|
+
]
|
|
2020
|
+
}
|
|
2021
|
+
];
|
|
2022
|
+
const result = await this.batchOnDb(dbName, statements, { signal });
|
|
2023
|
+
if (!result.ok) return result;
|
|
2024
|
+
return ok(void 0);
|
|
2025
|
+
}
|
|
2026
|
+
async ensureMigrationsTable(dbName, signal) {
|
|
2027
|
+
const result = await this.batchOnDb(
|
|
2028
|
+
dbName,
|
|
2029
|
+
[
|
|
2030
|
+
{ sql: MIGRATIONS_SCHEMA },
|
|
2031
|
+
{
|
|
2032
|
+
sql: `INSERT OR REPLACE INTO ${MIGRATIONS_TABLE} (key, namespace, id, applied_at, statement_count) VALUES (?, ?, ?, ?, ?)`,
|
|
2033
|
+
params: [
|
|
2034
|
+
migrationKey(MIGRATIONS_META_NAMESPACE, MIGRATIONS_META_ID),
|
|
2035
|
+
MIGRATIONS_META_NAMESPACE,
|
|
2036
|
+
MIGRATIONS_META_ID,
|
|
2037
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
2038
|
+
1
|
|
2039
|
+
]
|
|
2040
|
+
}
|
|
2041
|
+
],
|
|
2042
|
+
{ signal }
|
|
2043
|
+
);
|
|
2044
|
+
if (!result.ok) return result;
|
|
2045
|
+
return ok(void 0);
|
|
2046
|
+
}
|
|
1849
2047
|
// === Private helpers ===
|
|
1850
|
-
async invokeSQL(dbName,
|
|
2048
|
+
async invokeSQL(dbName, actions, body, signal) {
|
|
1851
2049
|
const session = this.context.session;
|
|
1852
|
-
const
|
|
2050
|
+
const actionList = Array.isArray(actions) ? actions : [actions];
|
|
2051
|
+
const headers = actionList.length === 1 ? this.context.invoke(session, "sql", dbName, actionList[0]) : this.invokeSQLAny(session, dbName, actionList);
|
|
1853
2052
|
return this.context.fetch(`${this.host}/invoke`, {
|
|
1854
2053
|
method: "POST",
|
|
1855
2054
|
headers: {
|
|
@@ -1861,26 +2060,50 @@ var SQLService = class extends BaseService {
|
|
|
1861
2060
|
});
|
|
1862
2061
|
}
|
|
1863
2062
|
actionForSql(sql, fallback) {
|
|
1864
|
-
|
|
2063
|
+
const token = firstSqlToken(sql);
|
|
2064
|
+
if (token === "pragma") return SQLAction.ADMIN;
|
|
2065
|
+
if (token !== void 0 && DDL_TOKENS.has(token)) return SQLAction.DDL;
|
|
2066
|
+
return fallback;
|
|
2067
|
+
}
|
|
2068
|
+
actionsForSqlBatch(statements) {
|
|
2069
|
+
return this.dedupeActions(
|
|
2070
|
+
statements.map((statement) => this.actionForSql(statement.sql, SQLAction.WRITE))
|
|
2071
|
+
);
|
|
2072
|
+
}
|
|
2073
|
+
dedupeActions(actions) {
|
|
2074
|
+
return [...new Set(actions)];
|
|
1865
2075
|
}
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
2076
|
+
invokeSQLAny(session, dbName, actions) {
|
|
2077
|
+
if (!this.context.invokeAny) {
|
|
2078
|
+
throw new Error(
|
|
2079
|
+
`SQL operation requires multiple permissions (${actions.join(", ")}) but this SDK runtime does not support multi-resource invocations`
|
|
2080
|
+
);
|
|
2081
|
+
}
|
|
2082
|
+
return this.context.invokeAny(
|
|
2083
|
+
session,
|
|
2084
|
+
actions.map((action) => ({
|
|
2085
|
+
spaceId: session.spaceId,
|
|
2086
|
+
service: "sql",
|
|
2087
|
+
path: dbName,
|
|
2088
|
+
action
|
|
2089
|
+
}))
|
|
2090
|
+
);
|
|
1870
2091
|
}
|
|
1871
2092
|
async handleErrorResponse(response, operation) {
|
|
1872
2093
|
const errorText = await response.text();
|
|
1873
|
-
|
|
1874
|
-
try {
|
|
1875
|
-
errorBody = JSON.parse(errorText);
|
|
1876
|
-
} catch {
|
|
1877
|
-
}
|
|
2094
|
+
const errorBody = parseServiceErrorBody(errorText);
|
|
1878
2095
|
const errorCode = this.mapHttpStatusToErrorCode(
|
|
1879
2096
|
response.status,
|
|
1880
2097
|
errorBody.error
|
|
1881
2098
|
);
|
|
1882
|
-
const message =
|
|
1883
|
-
|
|
2099
|
+
const message = formatServiceResponseError(
|
|
2100
|
+
"SQL",
|
|
2101
|
+
operation,
|
|
2102
|
+
response.status,
|
|
2103
|
+
errorText,
|
|
2104
|
+
errorBody
|
|
2105
|
+
);
|
|
2106
|
+
const meta = responseErrorMeta(response.status, response.statusText, errorText);
|
|
1884
2107
|
if (response.status === 401) {
|
|
1885
2108
|
const { resource, action } = parseAuthError(errorText);
|
|
1886
2109
|
if (action) meta.requiredAction = action;
|
|
@@ -1940,6 +2163,53 @@ function firstSqlToken(sql) {
|
|
|
1940
2163
|
const match = /^[A-Za-z_]+/.exec(sql.slice(index));
|
|
1941
2164
|
return match?.[0].toLowerCase();
|
|
1942
2165
|
}
|
|
2166
|
+
function validateMigrationOptions(options) {
|
|
2167
|
+
if (!options || typeof options !== "object") {
|
|
2168
|
+
return "SQL migrations require options";
|
|
2169
|
+
}
|
|
2170
|
+
if (!options.namespace || typeof options.namespace !== "string") {
|
|
2171
|
+
return "SQL migrations require a non-empty namespace";
|
|
2172
|
+
}
|
|
2173
|
+
if (!Array.isArray(options.migrations)) {
|
|
2174
|
+
return "SQL migrations require an ordered migrations array";
|
|
2175
|
+
}
|
|
2176
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2177
|
+
for (const migration of options.migrations) {
|
|
2178
|
+
if (!migration.id || typeof migration.id !== "string") {
|
|
2179
|
+
return "SQL migrations require every migration to have a non-empty id";
|
|
2180
|
+
}
|
|
2181
|
+
if (ids.has(migration.id)) {
|
|
2182
|
+
return `Duplicate SQL migration id: ${migration.id}`;
|
|
2183
|
+
}
|
|
2184
|
+
ids.add(migration.id);
|
|
2185
|
+
if (!Array.isArray(migration.sql)) {
|
|
2186
|
+
return `SQL migration ${migration.id} requires a sql array`;
|
|
2187
|
+
}
|
|
2188
|
+
for (const statement of migration.sql) {
|
|
2189
|
+
if (typeof statement === "string") {
|
|
2190
|
+
if (statement.trim() === "") {
|
|
2191
|
+
return `SQL migration ${migration.id} contains an empty statement`;
|
|
2192
|
+
}
|
|
2193
|
+
continue;
|
|
2194
|
+
}
|
|
2195
|
+
if (!statement || typeof statement.sql !== "string" || statement.sql.trim() === "") {
|
|
2196
|
+
return `SQL migration ${migration.id} contains an invalid statement`;
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
return null;
|
|
2201
|
+
}
|
|
2202
|
+
function migrationKey(namespace, id) {
|
|
2203
|
+
return `${encodeURIComponent(namespace)}:${encodeURIComponent(id)}`;
|
|
2204
|
+
}
|
|
2205
|
+
function rowValue(row, index) {
|
|
2206
|
+
if (Array.isArray(row)) return row[index];
|
|
2207
|
+
if (row && typeof row === "object") {
|
|
2208
|
+
const values = Object.values(row);
|
|
2209
|
+
return values[index];
|
|
2210
|
+
}
|
|
2211
|
+
return void 0;
|
|
2212
|
+
}
|
|
1943
2213
|
|
|
1944
2214
|
// src/duckdb/DuckDbDatabaseHandle.ts
|
|
1945
2215
|
var DuckDbDatabaseHandle = class {
|
|
@@ -2249,17 +2519,19 @@ var DuckDbService = class extends BaseService {
|
|
|
2249
2519
|
}
|
|
2250
2520
|
async handleErrorResponse(response, operation) {
|
|
2251
2521
|
const errorText = await response.text();
|
|
2252
|
-
|
|
2253
|
-
try {
|
|
2254
|
-
errorBody = JSON.parse(errorText);
|
|
2255
|
-
} catch {
|
|
2256
|
-
}
|
|
2522
|
+
const errorBody = parseServiceErrorBody(errorText);
|
|
2257
2523
|
const errorCode = this.mapHttpStatusToErrorCode(
|
|
2258
2524
|
response.status,
|
|
2259
2525
|
errorBody.error
|
|
2260
2526
|
);
|
|
2261
|
-
const message =
|
|
2262
|
-
|
|
2527
|
+
const message = formatServiceResponseError(
|
|
2528
|
+
"DuckDB",
|
|
2529
|
+
operation,
|
|
2530
|
+
response.status,
|
|
2531
|
+
errorText,
|
|
2532
|
+
errorBody
|
|
2533
|
+
);
|
|
2534
|
+
const meta = responseErrorMeta(response.status, response.statusText, errorText);
|
|
2263
2535
|
if (response.status === 401) {
|
|
2264
2536
|
const { resource, action } = parseAuthError(errorText);
|
|
2265
2537
|
if (action) meta.requiredAction = action;
|
|
@@ -5607,6 +5879,7 @@ export {
|
|
|
5607
5879
|
RetryPolicySchema,
|
|
5608
5880
|
SECRET_NAME_RE,
|
|
5609
5881
|
SQLAction,
|
|
5882
|
+
SQLMigrations,
|
|
5610
5883
|
SQLService,
|
|
5611
5884
|
SecretsService,
|
|
5612
5885
|
ServiceContext,
|