@tinycloud/sdk-services 2.3.0-beta.5 → 2.3.0-beta.7

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 CHANGED
@@ -921,6 +921,18 @@ var PrefixedKVService = class _PrefixedKVService {
921
921
  const fullKey = this.getFullKey(key);
922
922
  return this._kv.put(fullKey, value, { ...options, prefix: "" });
923
923
  }
924
+ /**
925
+ * Store multiple values within this prefix in one TinyCloud KV invocation.
926
+ */
927
+ async batchPut(items, options) {
928
+ return this._kv.batchPut(
929
+ items.map((item) => ({
930
+ ...item,
931
+ key: this.getFullKey(item.key)
932
+ })),
933
+ { ...options, prefix: "" }
934
+ );
935
+ }
924
936
  /**
925
937
  * List keys within this prefix.
926
938
  */
@@ -974,6 +986,12 @@ var KVAction = {
974
986
  };
975
987
 
976
988
  // src/kv/KVService.ts
989
+ function encodeKvBatchPartName(path) {
990
+ return encodeURIComponent(path).replace(
991
+ /[!'()*]/g,
992
+ (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`
993
+ );
994
+ }
977
995
  var KVService = class extends BaseService {
978
996
  /**
979
997
  * Create a new KVService instance.
@@ -1082,6 +1100,53 @@ var KVService = class extends BaseService {
1082
1100
  signal: this.combineSignals(signal)
1083
1101
  });
1084
1102
  }
1103
+ serializeBatchPutValue(item) {
1104
+ const contentType = item.contentType;
1105
+ if (item.value instanceof Blob) {
1106
+ if (!contentType || item.value.type === contentType) {
1107
+ return item.value;
1108
+ }
1109
+ return new Blob([item.value], { type: contentType });
1110
+ }
1111
+ if (item.value instanceof ArrayBuffer) {
1112
+ return new Blob([item.value], {
1113
+ type: contentType ?? "application/octet-stream"
1114
+ });
1115
+ }
1116
+ if (ArrayBuffer.isView(item.value)) {
1117
+ const value = item.value;
1118
+ const bytes = new Uint8Array(value.byteLength);
1119
+ bytes.set(new Uint8Array(value.buffer, value.byteOffset, value.byteLength));
1120
+ return new Blob([bytes], {
1121
+ type: contentType ?? "application/octet-stream"
1122
+ });
1123
+ }
1124
+ if (typeof item.value === "string") {
1125
+ return new Blob([item.value], {
1126
+ type: contentType ?? "text/plain;charset=UTF-8"
1127
+ });
1128
+ }
1129
+ const json = JSON.stringify(item.value);
1130
+ if (json === void 0) {
1131
+ throw new Error(`Cannot JSON serialize KV batch value for key "${item.key}"`);
1132
+ }
1133
+ return new Blob([json], {
1134
+ type: contentType ?? "application/json"
1135
+ });
1136
+ }
1137
+ normalizeBatchPutResponse(data) {
1138
+ if (!data || typeof data !== "object") {
1139
+ return void 0;
1140
+ }
1141
+ const response = data;
1142
+ if (!Array.isArray(response.written) || !response.written.every((key) => typeof key === "string") || typeof response.count !== "number") {
1143
+ return void 0;
1144
+ }
1145
+ return {
1146
+ written: response.written,
1147
+ count: response.count
1148
+ };
1149
+ }
1085
1150
  /**
1086
1151
  * Create KVResponseHeaders from fetch response headers.
1087
1152
  *
@@ -1283,6 +1348,107 @@ var KVService = class extends BaseService {
1283
1348
  }
1284
1349
  });
1285
1350
  }
1351
+ /**
1352
+ * Store multiple values in one TinyCloud KV invocation.
1353
+ */
1354
+ async batchPut(items, options) {
1355
+ return this.withTelemetry("batchPut", String(items.length), async () => {
1356
+ if (!this.requireAuth()) {
1357
+ return err(authRequiredError("kv"));
1358
+ }
1359
+ if (items.length === 0) {
1360
+ return ok({ written: [], count: 0 });
1361
+ }
1362
+ if (!this.context.invokeAny) {
1363
+ return err(
1364
+ serviceError(
1365
+ ErrorCodes.INVALID_INPUT,
1366
+ "KV batchPut requires SDK runtime support for multi-resource invocations",
1367
+ "kv"
1368
+ )
1369
+ );
1370
+ }
1371
+ const session = this.context.session;
1372
+ const paths = items.map((item) => this.getFullPath(item.key, options?.prefix));
1373
+ const seen = /* @__PURE__ */ new Set();
1374
+ for (const path of paths) {
1375
+ if (seen.has(path)) {
1376
+ return err(
1377
+ serviceError(
1378
+ ErrorCodes.INVALID_INPUT,
1379
+ `KV batchPut received duplicate key after prefix resolution: ${path}`,
1380
+ "kv"
1381
+ )
1382
+ );
1383
+ }
1384
+ seen.add(path);
1385
+ }
1386
+ try {
1387
+ const body = new FormData();
1388
+ for (let index = 0; index < items.length; index++) {
1389
+ body.append(
1390
+ encodeKvBatchPartName(paths[index]),
1391
+ this.serializeBatchPutValue(items[index])
1392
+ );
1393
+ }
1394
+ const headers = this.context.invokeAny(
1395
+ session,
1396
+ paths.map((path) => ({
1397
+ spaceId: session.spaceId,
1398
+ service: "kv",
1399
+ path,
1400
+ action: KVAction.PUT
1401
+ }))
1402
+ );
1403
+ const response = await this.context.fetch(`${this.host}/invoke`, {
1404
+ method: "POST",
1405
+ headers,
1406
+ body,
1407
+ signal: this.combineSignals(options?.signal)
1408
+ });
1409
+ if (!response.ok) {
1410
+ const errorText = await response.text();
1411
+ if (response.status === 401 || response.status === 403) {
1412
+ const { resource, action } = parseAuthError(errorText);
1413
+ return err(authUnauthorizedError("kv", errorText, {
1414
+ status: response.status,
1415
+ ...action && { requiredAction: action },
1416
+ ...resource && { resource }
1417
+ }));
1418
+ }
1419
+ const quotaError = this.handleQuotaErrorResponse(
1420
+ response,
1421
+ errorText,
1422
+ "batch"
1423
+ );
1424
+ if (quotaError) {
1425
+ return quotaError;
1426
+ }
1427
+ return err(
1428
+ serviceError(
1429
+ ErrorCodes.KV_WRITE_FAILED,
1430
+ `Failed to batch put ${items.length} key(s): ${response.status} - ${errorText}`,
1431
+ "kv",
1432
+ { meta: { status: response.status, statusText: response.statusText } }
1433
+ )
1434
+ );
1435
+ }
1436
+ const batchResponse = this.normalizeBatchPutResponse(await response.json());
1437
+ if (!batchResponse || batchResponse.count !== batchResponse.written.length) {
1438
+ return err(
1439
+ serviceError(
1440
+ ErrorCodes.NETWORK_ERROR,
1441
+ "KV batchPut response did not include matching written keys and count",
1442
+ "kv"
1443
+ )
1444
+ );
1445
+ }
1446
+ return ok(batchResponse);
1447
+ } catch (error) {
1448
+ return err(wrapError("kv", error));
1449
+ }
1450
+ });
1451
+ }
1286
1452
  /**
1287
1453
  * List keys with optional prefix filtering.
1288
1454
  */
@@ -1632,7 +1798,7 @@ var SQLService = class extends BaseService {
1632
1798
  try {
1633
1799
  const response = await this.invokeSQL(
1634
1800
  dbName,
1635
- SQLAction.READ,
1801
+ this.actionForSql(sql, SQLAction.READ),
1636
1802
  { action: "query", sql, params: params ?? [] },
1637
1803
  options?.signal
1638
1804
  );
@@ -1662,7 +1828,7 @@ var SQLService = class extends BaseService {
1662
1828
  }
1663
1829
  const response = await this.invokeSQL(
1664
1830
  dbName,
1665
- SQLAction.WRITE,
1831
+ this.actionForSql(sql, SQLAction.WRITE),
1666
1832
  body,
1667
1833
  options?.signal
1668
1834
  );
@@ -1684,7 +1850,7 @@ var SQLService = class extends BaseService {
1684
1850
  try {
1685
1851
  const response = await this.invokeSQL(
1686
1852
  dbName,
1687
- SQLAction.WRITE,
1853
+ this.actionForSqlBatch(statements),
1688
1854
  { action: "batch", statements },
1689
1855
  options?.signal
1690
1856
  );
@@ -1761,6 +1927,14 @@ var SQLService = class extends BaseService {
1761
1927
  signal: this.combineSignals(signal)
1762
1928
  });
1763
1929
  }
1930
+ actionForSql(sql, fallback) {
1931
+ return firstSqlToken(sql) === "pragma" ? SQLAction.ADMIN : fallback;
1932
+ }
1933
+ actionForSqlBatch(statements) {
1934
+ return statements.some(
1935
+ (statement) => this.actionForSql(statement.sql, SQLAction.WRITE) === SQLAction.ADMIN
1936
+ ) ? SQLAction.ADMIN : SQLAction.WRITE;
1937
+ }
1764
1938
  async handleErrorResponse(response, operation) {
1765
1939
  const errorText = await response.text();
1766
1940
  let errorBody = {};
@@ -1806,6 +1980,33 @@ var SQLService = class extends BaseService {
1806
1980
  }
1807
1981
  };
1808
1982
  SQLService.serviceName = "sql";
1983
+ function firstSqlToken(sql) {
1984
+ let index = 0;
1985
+ while (index < sql.length) {
1986
+ while (index < sql.length && /\s/.test(sql[index])) {
1987
+ index++;
1988
+ }
1989
+ if (sql.startsWith("--", index)) {
1990
+ const newline = sql.indexOf("\n", index + 2);
1991
+ if (newline === -1) {
1992
+ return void 0;
1993
+ }
1994
+ index = newline + 1;
1995
+ continue;
1996
+ }
1997
+ if (sql.startsWith("/*", index)) {
1998
+ const end = sql.indexOf("*/", index + 2);
1999
+ if (end === -1) {
2000
+ return void 0;
2001
+ }
2002
+ index = end + 2;
2003
+ continue;
2004
+ }
2005
+ break;
2006
+ }
2007
+ const match = /^[A-Za-z_]+/.exec(sql.slice(index));
2008
+ return match?.[0].toLowerCase();
2009
+ }
1809
2010
 
1810
2011
  // src/duckdb/DuckDbDatabaseHandle.ts
1811
2012
  var DuckDbDatabaseHandle = class {