@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.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 err(
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(response, options?.raw);
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
- let body;
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 err(
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 err(
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.actionForSql(sql, SQLAction.WRITE),
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.actionForSqlBatch(statements),
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, action, body, signal) {
2048
+ async invokeSQL(dbName, actions, body, signal) {
1851
2049
  const session = this.context.session;
1852
- const headers = this.context.invoke(session, "sql", dbName, action);
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
- return firstSqlToken(sql) === "pragma" ? SQLAction.ADMIN : fallback;
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
- actionForSqlBatch(statements) {
1867
- return statements.some(
1868
- (statement) => this.actionForSql(statement.sql, SQLAction.WRITE) === SQLAction.ADMIN
1869
- ) ? SQLAction.ADMIN : SQLAction.WRITE;
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
- let errorBody = {};
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 = errorBody.message || `SQL ${operation} failed: ${response.status} - ${errorText}`;
1883
- const meta = { status: response.status, statusText: response.statusText };
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
- let errorBody = {};
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 = errorBody.message || `DuckDB ${operation} failed: ${response.status} - ${errorText}`;
2262
- const meta = { status: response.status, statusText: response.statusText };
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,