@tinycloud/sdk-services 2.4.0-beta.1 → 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 +284 -44
- 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 +283 -44
- package/dist/index.js.map +1 -1
- package/dist/kv/index.cjs +28 -21
- package/dist/kv/index.cjs.map +1 -1
- package/dist/kv/index.d.cts +12 -0
- package/dist/kv/index.d.ts +12 -0
- package/dist/kv/index.js +28 -21
- 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
|
*
|
|
@@ -1229,13 +1254,7 @@ var KVService = class extends BaseService {
|
|
|
1229
1254
|
}));
|
|
1230
1255
|
}
|
|
1231
1256
|
if (response.status === 404) {
|
|
1232
|
-
return
|
|
1233
|
-
serviceError(
|
|
1234
|
-
ErrorCodes.KV_NOT_FOUND,
|
|
1235
|
-
`Key not found: ${key}`,
|
|
1236
|
-
"kv"
|
|
1237
|
-
)
|
|
1238
|
-
);
|
|
1257
|
+
return this.classifyNotFound(response, key);
|
|
1239
1258
|
}
|
|
1240
1259
|
const errorText = await response.text();
|
|
1241
1260
|
return err(
|
|
@@ -1496,13 +1515,7 @@ var KVService = class extends BaseService {
|
|
|
1496
1515
|
}));
|
|
1497
1516
|
}
|
|
1498
1517
|
if (response.status === 404) {
|
|
1499
|
-
return
|
|
1500
|
-
serviceError(
|
|
1501
|
-
ErrorCodes.KV_NOT_FOUND,
|
|
1502
|
-
`Key not found: ${key}`,
|
|
1503
|
-
"kv"
|
|
1504
|
-
)
|
|
1505
|
-
);
|
|
1518
|
+
return this.classifyNotFound(response, key);
|
|
1506
1519
|
}
|
|
1507
1520
|
const errorText = await response.text();
|
|
1508
1521
|
return err(
|
|
@@ -1547,13 +1560,7 @@ var KVService = class extends BaseService {
|
|
|
1547
1560
|
}));
|
|
1548
1561
|
}
|
|
1549
1562
|
if (response.status === 404) {
|
|
1550
|
-
return
|
|
1551
|
-
serviceError(
|
|
1552
|
-
ErrorCodes.KV_NOT_FOUND,
|
|
1553
|
-
`Key not found: ${key}`,
|
|
1554
|
-
"kv"
|
|
1555
|
-
)
|
|
1556
|
-
);
|
|
1563
|
+
return this.classifyNotFound(response, key);
|
|
1557
1564
|
}
|
|
1558
1565
|
const errorText = await response.text();
|
|
1559
1566
|
return err(
|
|
@@ -1680,11 +1687,59 @@ var KVService = class extends BaseService {
|
|
|
1680
1687
|
*/
|
|
1681
1688
|
KVService.serviceName = "kv";
|
|
1682
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
|
+
|
|
1683
1737
|
// src/sql/DatabaseHandle.ts
|
|
1684
1738
|
var DatabaseHandle = class {
|
|
1685
1739
|
constructor(service, name) {
|
|
1686
1740
|
this.service = service;
|
|
1687
1741
|
this.name = name;
|
|
1742
|
+
this.migrations = new SQLMigrations(service, name);
|
|
1688
1743
|
}
|
|
1689
1744
|
async query(sql, params, options) {
|
|
1690
1745
|
return this.service.queryOnDb(this.name, sql, params, options);
|
|
@@ -1707,6 +1762,7 @@ var DatabaseHandle = class {
|
|
|
1707
1762
|
var SQLAction = {
|
|
1708
1763
|
READ: "tinycloud.sql/read",
|
|
1709
1764
|
WRITE: "tinycloud.sql/write",
|
|
1765
|
+
DDL: "tinycloud.sql/ddl",
|
|
1710
1766
|
ADMIN: "tinycloud.sql/admin",
|
|
1711
1767
|
SELECT: "tinycloud.sql/select",
|
|
1712
1768
|
INSERT: "tinycloud.sql/insert",
|
|
@@ -1718,9 +1774,21 @@ var SQLAction = {
|
|
|
1718
1774
|
};
|
|
1719
1775
|
|
|
1720
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";
|
|
1721
1788
|
var SQLService = class extends BaseService {
|
|
1722
1789
|
constructor(config = {}) {
|
|
1723
1790
|
super();
|
|
1791
|
+
this.migrationLocks = /* @__PURE__ */ new Map();
|
|
1724
1792
|
this._config = config;
|
|
1725
1793
|
}
|
|
1726
1794
|
get config() {
|
|
@@ -1756,6 +1824,24 @@ var SQLService = class extends BaseService {
|
|
|
1756
1824
|
async batch(statements, options) {
|
|
1757
1825
|
return this.batchOnDb(this.defaultDbName, statements, options);
|
|
1758
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
|
+
}
|
|
1759
1845
|
// === Internal methods called by DatabaseHandle ===
|
|
1760
1846
|
async queryOnDb(dbName, sql, params, options) {
|
|
1761
1847
|
return this.withTelemetry("query", dbName, async () => {
|
|
@@ -1793,9 +1879,15 @@ var SQLService = class extends BaseService {
|
|
|
1793
1879
|
if (options?.schema) {
|
|
1794
1880
|
body.schema = options.schema;
|
|
1795
1881
|
}
|
|
1882
|
+
const actions = [
|
|
1883
|
+
this.actionForSql(sql, SQLAction.WRITE),
|
|
1884
|
+
...(options?.schema ?? []).map(
|
|
1885
|
+
(statement) => this.actionForSql(statement, SQLAction.DDL)
|
|
1886
|
+
)
|
|
1887
|
+
];
|
|
1796
1888
|
const response = await this.invokeSQL(
|
|
1797
1889
|
dbName,
|
|
1798
|
-
this.
|
|
1890
|
+
this.dedupeActions(actions),
|
|
1799
1891
|
body,
|
|
1800
1892
|
options?.signal
|
|
1801
1893
|
);
|
|
@@ -1817,7 +1909,7 @@ var SQLService = class extends BaseService {
|
|
|
1817
1909
|
try {
|
|
1818
1910
|
const response = await this.invokeSQL(
|
|
1819
1911
|
dbName,
|
|
1820
|
-
this.
|
|
1912
|
+
this.actionsForSqlBatch(statements),
|
|
1821
1913
|
{ action: "batch", statements },
|
|
1822
1914
|
options?.signal
|
|
1823
1915
|
);
|
|
@@ -1880,10 +1972,83 @@ var SQLService = class extends BaseService {
|
|
|
1880
1972
|
}
|
|
1881
1973
|
});
|
|
1882
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
|
+
}
|
|
1883
2047
|
// === Private helpers ===
|
|
1884
|
-
async invokeSQL(dbName,
|
|
2048
|
+
async invokeSQL(dbName, actions, body, signal) {
|
|
1885
2049
|
const session = this.context.session;
|
|
1886
|
-
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);
|
|
1887
2052
|
return this.context.fetch(`${this.host}/invoke`, {
|
|
1888
2053
|
method: "POST",
|
|
1889
2054
|
headers: {
|
|
@@ -1895,26 +2060,50 @@ var SQLService = class extends BaseService {
|
|
|
1895
2060
|
});
|
|
1896
2061
|
}
|
|
1897
2062
|
actionForSql(sql, fallback) {
|
|
1898
|
-
|
|
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)];
|
|
1899
2075
|
}
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
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
|
+
);
|
|
1904
2091
|
}
|
|
1905
2092
|
async handleErrorResponse(response, operation) {
|
|
1906
2093
|
const errorText = await response.text();
|
|
1907
|
-
|
|
1908
|
-
try {
|
|
1909
|
-
errorBody = JSON.parse(errorText);
|
|
1910
|
-
} catch {
|
|
1911
|
-
}
|
|
2094
|
+
const errorBody = parseServiceErrorBody(errorText);
|
|
1912
2095
|
const errorCode = this.mapHttpStatusToErrorCode(
|
|
1913
2096
|
response.status,
|
|
1914
2097
|
errorBody.error
|
|
1915
2098
|
);
|
|
1916
|
-
const message =
|
|
1917
|
-
|
|
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);
|
|
1918
2107
|
if (response.status === 401) {
|
|
1919
2108
|
const { resource, action } = parseAuthError(errorText);
|
|
1920
2109
|
if (action) meta.requiredAction = action;
|
|
@@ -1974,6 +2163,53 @@ function firstSqlToken(sql) {
|
|
|
1974
2163
|
const match = /^[A-Za-z_]+/.exec(sql.slice(index));
|
|
1975
2164
|
return match?.[0].toLowerCase();
|
|
1976
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
|
+
}
|
|
1977
2213
|
|
|
1978
2214
|
// src/duckdb/DuckDbDatabaseHandle.ts
|
|
1979
2215
|
var DuckDbDatabaseHandle = class {
|
|
@@ -2283,17 +2519,19 @@ var DuckDbService = class extends BaseService {
|
|
|
2283
2519
|
}
|
|
2284
2520
|
async handleErrorResponse(response, operation) {
|
|
2285
2521
|
const errorText = await response.text();
|
|
2286
|
-
|
|
2287
|
-
try {
|
|
2288
|
-
errorBody = JSON.parse(errorText);
|
|
2289
|
-
} catch {
|
|
2290
|
-
}
|
|
2522
|
+
const errorBody = parseServiceErrorBody(errorText);
|
|
2291
2523
|
const errorCode = this.mapHttpStatusToErrorCode(
|
|
2292
2524
|
response.status,
|
|
2293
2525
|
errorBody.error
|
|
2294
2526
|
);
|
|
2295
|
-
const message =
|
|
2296
|
-
|
|
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);
|
|
2297
2535
|
if (response.status === 401) {
|
|
2298
2536
|
const { resource, action } = parseAuthError(errorText);
|
|
2299
2537
|
if (action) meta.requiredAction = action;
|
|
@@ -5641,6 +5879,7 @@ export {
|
|
|
5641
5879
|
RetryPolicySchema,
|
|
5642
5880
|
SECRET_NAME_RE,
|
|
5643
5881
|
SQLAction,
|
|
5882
|
+
SQLMigrations,
|
|
5644
5883
|
SQLService,
|
|
5645
5884
|
SecretsService,
|
|
5646
5885
|
ServiceContext,
|