@tinycloud/sdk-services 2.4.0-beta.2 → 2.4.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/index.js CHANGED
@@ -67,6 +67,202 @@ function serviceError(code, message, service, options) {
67
67
  };
68
68
  }
69
69
 
70
+ // src/debug.ts
71
+ var DEBUG_FLAG = "TinyCloud_debug";
72
+ var MAX_EVENTS = 1e3;
73
+ function getGlobal() {
74
+ return globalThis;
75
+ }
76
+ function nowMs() {
77
+ const performanceNow = globalThis.performance?.now?.bind(globalThis.performance);
78
+ return typeof performanceNow === "function" ? performanceNow() : Date.now();
79
+ }
80
+ function isTrue(value) {
81
+ return value === true || value === "true" || value === "1";
82
+ }
83
+ function getProcessDebugFlag() {
84
+ const processLike = globalThis.process;
85
+ return processLike?.env?.[DEBUG_FLAG];
86
+ }
87
+ function isBrowserWindow() {
88
+ const global = getGlobal();
89
+ return global.window === globalThis;
90
+ }
91
+ function getLocalStorage() {
92
+ if (!isBrowserWindow()) {
93
+ return void 0;
94
+ }
95
+ try {
96
+ return globalThis.localStorage;
97
+ } catch {
98
+ return void 0;
99
+ }
100
+ }
101
+ function getStoredDebugFlag() {
102
+ const storage = getLocalStorage();
103
+ if (!storage) {
104
+ return void 0;
105
+ }
106
+ try {
107
+ return storage.getItem(DEBUG_FLAG);
108
+ } catch {
109
+ return void 0;
110
+ }
111
+ }
112
+ function setStoredDebugFlag(enabled) {
113
+ const storage = getLocalStorage();
114
+ if (!storage) {
115
+ return;
116
+ }
117
+ try {
118
+ if (enabled) {
119
+ storage.setItem(DEBUG_FLAG, "true");
120
+ } else {
121
+ storage.removeItem(DEBUG_FLAG);
122
+ }
123
+ } catch {
124
+ }
125
+ }
126
+ function shouldStartEnabled() {
127
+ const global = getGlobal();
128
+ return isTrue(global[DEBUG_FLAG]) || isTrue(getStoredDebugFlag()) || isTrue(getProcessDebugFlag());
129
+ }
130
+ var TinyCloudDebugLogger = class {
131
+ constructor() {
132
+ this.enabled = shouldStartEnabled();
133
+ this.sequence = 0;
134
+ this.events = [];
135
+ this.maxEvents = MAX_EVENTS;
136
+ }
137
+ isEnabled() {
138
+ return this.enabled || isTrue(getGlobal()[DEBUG_FLAG]) || isTrue(getProcessDebugFlag());
139
+ }
140
+ enable(options = {}) {
141
+ this.enabled = true;
142
+ getGlobal()[DEBUG_FLAG] = true;
143
+ if (options.persist !== false) {
144
+ setStoredDebugFlag(true);
145
+ }
146
+ this.log("debug.enabled", { persisted: options.persist !== false });
147
+ }
148
+ disable(options = {}) {
149
+ this.log("debug.disabled", { persisted: options.persist !== false });
150
+ this.enabled = false;
151
+ getGlobal()[DEBUG_FLAG] = false;
152
+ if (options.persist !== false) {
153
+ setStoredDebugFlag(false);
154
+ }
155
+ }
156
+ clear() {
157
+ this.events = [];
158
+ }
159
+ getLogs() {
160
+ return [...this.events];
161
+ }
162
+ log(event, data, message) {
163
+ if (!this.isEnabled()) {
164
+ return void 0;
165
+ }
166
+ return this.record({
167
+ event,
168
+ data,
169
+ message,
170
+ level: "debug",
171
+ timestamp: Date.now(),
172
+ timestampIso: (/* @__PURE__ */ new Date()).toISOString()
173
+ });
174
+ }
175
+ startTimer(event, data) {
176
+ if (!this.isEnabled()) {
177
+ return { stop: () => void 0 };
178
+ }
179
+ const startedAt = nowMs();
180
+ this.log(`${event}.start`, data);
181
+ return {
182
+ stop: (finishData) => {
183
+ const endedAt = nowMs();
184
+ return this.record({
185
+ event: `${event}.end`,
186
+ data: finishData,
187
+ durationMs: endedAt - startedAt,
188
+ startedAt,
189
+ endedAt,
190
+ level: "debug",
191
+ timestamp: Date.now(),
192
+ timestampIso: (/* @__PURE__ */ new Date()).toISOString()
193
+ });
194
+ }
195
+ };
196
+ }
197
+ async timeAsync(event, operation, data) {
198
+ if (!this.isEnabled()) {
199
+ return operation();
200
+ }
201
+ const timer = this.startTimer(event, data);
202
+ try {
203
+ const result = await operation();
204
+ timer.stop({ ok: true });
205
+ return result;
206
+ } catch (error) {
207
+ timer.stop({ ok: false, error });
208
+ throw error;
209
+ }
210
+ }
211
+ time(event, operation, data) {
212
+ if (!this.isEnabled()) {
213
+ return operation();
214
+ }
215
+ const timer = this.startTimer(event, data);
216
+ try {
217
+ const result = operation();
218
+ timer.stop({ ok: true });
219
+ return result;
220
+ } catch (error) {
221
+ timer.stop({ ok: false, error });
222
+ throw error;
223
+ }
224
+ }
225
+ record(event) {
226
+ const debugEvent = {
227
+ ...event,
228
+ sequence: ++this.sequence
229
+ };
230
+ this.events.push(debugEvent);
231
+ if (this.events.length > this.maxEvents) {
232
+ this.events.splice(0, this.events.length - this.maxEvents);
233
+ }
234
+ try {
235
+ globalThis.console?.debug?.("[TinyCloud]", debugEvent.event, debugEvent);
236
+ } catch {
237
+ }
238
+ return debugEvent;
239
+ }
240
+ };
241
+ var tinyCloudDebugLogger = new TinyCloudDebugLogger();
242
+ function enableTinyCloudDebug(options) {
243
+ tinyCloudDebugLogger.enable(options);
244
+ return tinyCloudDebugLogger;
245
+ }
246
+ function disableTinyCloudDebug(options) {
247
+ tinyCloudDebugLogger.disable(options);
248
+ return tinyCloudDebugLogger;
249
+ }
250
+ function getTinyCloudDebugLogs() {
251
+ return tinyCloudDebugLogger.getLogs();
252
+ }
253
+ function clearTinyCloudDebugLogs() {
254
+ tinyCloudDebugLogger.clear();
255
+ }
256
+ function installTinyCloudDebugGlobals() {
257
+ const global = getGlobal();
258
+ global.TinyCloudDebug = tinyCloudDebugLogger;
259
+ global.enableTinyCloudDebug = enableTinyCloudDebug;
260
+ global.disableTinyCloudDebug = disableTinyCloudDebug;
261
+ global.getTinyCloudDebugLogs = getTinyCloudDebugLogs;
262
+ global.clearTinyCloudDebugLogs = clearTinyCloudDebugLogs;
263
+ }
264
+ installTinyCloudDebugGlobals();
265
+
70
266
  // src/types.schema.ts
71
267
  import { z } from "zod";
72
268
  var ServiceErrorSchema = z.object({
@@ -308,9 +504,9 @@ var ServiceContext = class {
308
504
  this._services = /* @__PURE__ */ new Map();
309
505
  this._eventHandlers = /* @__PURE__ */ new Map();
310
506
  this._abortController = new AbortController();
311
- this._invoke = config.invoke;
312
- this._invokeAny = config.invokeAny;
313
- this._fetch = config.fetch ?? globalThis.fetch.bind(globalThis);
507
+ this._invoke = this.wrapInvoke(config.invoke);
508
+ this._invokeAny = config.invokeAny ? this.wrapInvokeAny(config.invokeAny) : void 0;
509
+ this._fetch = this.wrapFetch(config.fetch ?? globalThis.fetch.bind(globalThis));
314
510
  this._hosts = config.hosts;
315
511
  this._session = config.session ?? null;
316
512
  this._retryPolicy = {
@@ -413,6 +609,7 @@ var ServiceContext = class {
413
609
  * @param data - Event data
414
610
  */
415
611
  emit(event, data) {
612
+ tinyCloudDebugLogger.log(event, data);
416
613
  if (this._telemetryEnabled && this._telemetryHandler) {
417
614
  try {
418
615
  this._telemetryHandler(event, data);
@@ -502,6 +699,77 @@ var ServiceContext = class {
502
699
  get retryPolicy() {
503
700
  return this._retryPolicy;
504
701
  }
702
+ wrapInvoke(invoke) {
703
+ return (session, service, path, action, facts) => {
704
+ if (!tinyCloudDebugLogger.isEnabled()) {
705
+ return invoke(session, service, path, action, facts);
706
+ }
707
+ const timer = tinyCloudDebugLogger.startTimer("sdk.invoke", {
708
+ service,
709
+ path,
710
+ action,
711
+ facts
712
+ });
713
+ try {
714
+ const result = invoke(session, service, path, action, facts);
715
+ timer.stop({ ok: true, service, path, action });
716
+ return result;
717
+ } catch (error) {
718
+ timer.stop({ ok: false, service, path, action, error });
719
+ throw error;
720
+ }
721
+ };
722
+ }
723
+ wrapInvokeAny(invokeAny) {
724
+ return (session, entries, facts) => {
725
+ if (!tinyCloudDebugLogger.isEnabled()) {
726
+ return invokeAny(session, entries, facts);
727
+ }
728
+ const timer = tinyCloudDebugLogger.startTimer("sdk.invokeAny", {
729
+ entries,
730
+ facts
731
+ });
732
+ try {
733
+ const result = invokeAny(session, entries, facts);
734
+ timer.stop({ ok: true, entries });
735
+ return result;
736
+ } catch (error) {
737
+ timer.stop({ ok: false, entries, error });
738
+ throw error;
739
+ }
740
+ };
741
+ }
742
+ wrapFetch(fetchFn) {
743
+ return async (url, init) => {
744
+ if (!tinyCloudDebugLogger.isEnabled()) {
745
+ return fetchFn(url, init);
746
+ }
747
+ const timer = tinyCloudDebugLogger.startTimer("sdk.fetch", {
748
+ url,
749
+ method: init?.method ?? "GET",
750
+ init
751
+ });
752
+ try {
753
+ const response = await fetchFn(url, init);
754
+ timer.stop({
755
+ ok: response.ok,
756
+ url,
757
+ method: init?.method ?? "GET",
758
+ status: response.status,
759
+ statusText: response.statusText
760
+ });
761
+ return response;
762
+ } catch (error) {
763
+ timer.stop({
764
+ ok: false,
765
+ url,
766
+ method: init?.method ?? "GET",
767
+ error
768
+ });
769
+ throw error;
770
+ }
771
+ };
772
+ }
505
773
  };
506
774
 
507
775
  // src/errors.ts
@@ -1687,11 +1955,59 @@ var KVService = class extends BaseService {
1687
1955
  */
1688
1956
  KVService.serviceName = "kv";
1689
1957
 
1958
+ // src/responseErrors.ts
1959
+ function parseServiceErrorBody(errorText) {
1960
+ try {
1961
+ return JSON.parse(errorText);
1962
+ } catch {
1963
+ return {};
1964
+ }
1965
+ }
1966
+ function formatServiceResponseError(serviceLabel, operation, status, errorText, parsed) {
1967
+ if (parsed.message) {
1968
+ return compactMessage(parsed.message);
1969
+ }
1970
+ if (looksLikeHtml(errorText)) {
1971
+ if (status === 524 || /524\s*[:-]/i.test(errorText) || /a timeout occurred/i.test(errorText)) {
1972
+ return `${serviceLabel} ${operation} failed: upstream request timed out. Please retry.`;
1973
+ }
1974
+ return `${serviceLabel} ${operation} failed: upstream service returned an HTML error page (${status}).`;
1975
+ }
1976
+ return `${serviceLabel} ${operation} failed: ${status} - ${compactMessage(errorText)}`;
1977
+ }
1978
+ function responseErrorMeta(status, statusText, errorText) {
1979
+ const meta = { status, statusText };
1980
+ const snippet = compactMessage(errorText);
1981
+ if (snippet) {
1982
+ meta.responseSnippet = snippet.slice(0, 300);
1983
+ }
1984
+ return meta;
1985
+ }
1986
+ function looksLikeHtml(text) {
1987
+ return /<!doctype html/i.test(text) || /<html[\s>]/i.test(text);
1988
+ }
1989
+ function compactMessage(message) {
1990
+ const compact = message.replace(/\s+/g, " ").trim();
1991
+ return compact.length > 500 ? `${compact.slice(0, 500)}...` : compact;
1992
+ }
1993
+
1994
+ // src/sql/SQLMigrations.ts
1995
+ var SQLMigrations = class {
1996
+ constructor(service, dbName) {
1997
+ this.service = service;
1998
+ this.dbName = dbName;
1999
+ }
2000
+ apply(options) {
2001
+ return this.service.applyMigrationsOnDb(this.dbName, options);
2002
+ }
2003
+ };
2004
+
1690
2005
  // src/sql/DatabaseHandle.ts
1691
2006
  var DatabaseHandle = class {
1692
2007
  constructor(service, name) {
1693
2008
  this.service = service;
1694
2009
  this.name = name;
2010
+ this.migrations = new SQLMigrations(service, name);
1695
2011
  }
1696
2012
  async query(sql, params, options) {
1697
2013
  return this.service.queryOnDb(this.name, sql, params, options);
@@ -1714,6 +2030,7 @@ var DatabaseHandle = class {
1714
2030
  var SQLAction = {
1715
2031
  READ: "tinycloud.sql/read",
1716
2032
  WRITE: "tinycloud.sql/write",
2033
+ SCHEMA: "tinycloud.sql/schema",
1717
2034
  ADMIN: "tinycloud.sql/admin",
1718
2035
  SELECT: "tinycloud.sql/select",
1719
2036
  INSERT: "tinycloud.sql/insert",
@@ -1725,9 +2042,21 @@ var SQLAction = {
1725
2042
  };
1726
2043
 
1727
2044
  // src/sql/SQLService.ts
2045
+ var DDL_TOKENS = /* @__PURE__ */ new Set(["alter", "create", "drop"]);
2046
+ var MIGRATIONS_TABLE = "__tinycloud_sql_migrations";
2047
+ var MIGRATIONS_SCHEMA = `CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (
2048
+ key TEXT PRIMARY KEY,
2049
+ namespace TEXT NOT NULL,
2050
+ id TEXT NOT NULL,
2051
+ applied_at TEXT NOT NULL,
2052
+ statement_count INTEGER NOT NULL
2053
+ )`;
2054
+ var MIGRATIONS_META_NAMESPACE = "tinycloud.sql.migrations";
2055
+ var MIGRATIONS_META_ID = "000_metadata";
1728
2056
  var SQLService = class extends BaseService {
1729
2057
  constructor(config = {}) {
1730
2058
  super();
2059
+ this.migrationLocks = /* @__PURE__ */ new Map();
1731
2060
  this._config = config;
1732
2061
  }
1733
2062
  get config() {
@@ -1763,6 +2092,24 @@ var SQLService = class extends BaseService {
1763
2092
  async batch(statements, options) {
1764
2093
  return this.batchOnDb(this.defaultDbName, statements, options);
1765
2094
  }
2095
+ async applyMigrationsOnDb(dbName, options) {
2096
+ const validationError = validateMigrationOptions(options);
2097
+ if (validationError) {
2098
+ return err(serviceError(ErrorCodes.INVALID_INPUT, validationError, "sql"));
2099
+ }
2100
+ const lockKey = `${dbName}\0${options.namespace}`;
2101
+ const existing = this.migrationLocks.get(lockKey);
2102
+ if (existing) {
2103
+ return existing;
2104
+ }
2105
+ const promise = this.applyMigrationsOnDbUnlocked(dbName, options);
2106
+ this.migrationLocks.set(lockKey, promise);
2107
+ try {
2108
+ return await promise;
2109
+ } finally {
2110
+ this.migrationLocks.delete(lockKey);
2111
+ }
2112
+ }
1766
2113
  // === Internal methods called by DatabaseHandle ===
1767
2114
  async queryOnDb(dbName, sql, params, options) {
1768
2115
  return this.withTelemetry("query", dbName, async () => {
@@ -1800,9 +2147,15 @@ var SQLService = class extends BaseService {
1800
2147
  if (options?.schema) {
1801
2148
  body.schema = options.schema;
1802
2149
  }
2150
+ const actions = [
2151
+ this.actionForSql(sql, SQLAction.WRITE),
2152
+ ...(options?.schema ?? []).map(
2153
+ (statement) => this.actionForSql(statement, SQLAction.SCHEMA)
2154
+ )
2155
+ ];
1803
2156
  const response = await this.invokeSQL(
1804
2157
  dbName,
1805
- this.actionForSql(sql, SQLAction.WRITE),
2158
+ this.dedupeActions(actions),
1806
2159
  body,
1807
2160
  options?.signal
1808
2161
  );
@@ -1824,7 +2177,7 @@ var SQLService = class extends BaseService {
1824
2177
  try {
1825
2178
  const response = await this.invokeSQL(
1826
2179
  dbName,
1827
- this.actionForSqlBatch(statements),
2180
+ this.actionsForSqlBatch(statements),
1828
2181
  { action: "batch", statements },
1829
2182
  options?.signal
1830
2183
  );
@@ -1887,10 +2240,83 @@ var SQLService = class extends BaseService {
1887
2240
  }
1888
2241
  });
1889
2242
  }
2243
+ async applyMigrationsOnDbUnlocked(dbName, options) {
2244
+ return this.withTelemetry("migrations.apply", dbName, async () => {
2245
+ const created = await this.ensureMigrationsTable(dbName, options.signal);
2246
+ if (!created.ok) return created;
2247
+ const listed = await this.queryOnDb(
2248
+ dbName,
2249
+ `SELECT id FROM ${MIGRATIONS_TABLE} WHERE namespace = ? ORDER BY applied_at, id`,
2250
+ [options.namespace],
2251
+ { signal: options.signal }
2252
+ );
2253
+ if (!listed.ok) return listed;
2254
+ const appliedIds = new Set(
2255
+ listed.data.rows.map((row) => rowValue(row, 0)).filter((id) => typeof id === "string")
2256
+ );
2257
+ const skipped = options.migrations.filter((migration) => appliedIds.has(migration.id)).map((migration) => migration.id);
2258
+ const pending = options.migrations.filter((migration) => !appliedIds.has(migration.id));
2259
+ const applied = [];
2260
+ for (const migration of pending) {
2261
+ const result = await this.applyOneMigration(dbName, options.namespace, migration, options.signal);
2262
+ if (!result.ok) return result;
2263
+ applied.push(migration.id);
2264
+ }
2265
+ return ok({
2266
+ database: dbName,
2267
+ namespace: options.namespace,
2268
+ status: applied.length > 0 ? "applied" : "already_current",
2269
+ applied,
2270
+ skipped
2271
+ });
2272
+ });
2273
+ }
2274
+ async applyOneMigration(dbName, namespace, migration, signal) {
2275
+ const statements = [
2276
+ ...migration.sql.map(
2277
+ (statement) => typeof statement === "string" ? { sql: statement } : statement
2278
+ ),
2279
+ {
2280
+ sql: `INSERT OR REPLACE INTO ${MIGRATIONS_TABLE} (key, namespace, id, applied_at, statement_count) VALUES (?, ?, ?, ?, ?)`,
2281
+ params: [
2282
+ migrationKey(namespace, migration.id),
2283
+ namespace,
2284
+ migration.id,
2285
+ (/* @__PURE__ */ new Date()).toISOString(),
2286
+ migration.sql.length
2287
+ ]
2288
+ }
2289
+ ];
2290
+ const result = await this.batchOnDb(dbName, statements, { signal });
2291
+ if (!result.ok) return result;
2292
+ return ok(void 0);
2293
+ }
2294
+ async ensureMigrationsTable(dbName, signal) {
2295
+ const result = await this.batchOnDb(
2296
+ dbName,
2297
+ [
2298
+ { sql: MIGRATIONS_SCHEMA },
2299
+ {
2300
+ sql: `INSERT OR REPLACE INTO ${MIGRATIONS_TABLE} (key, namespace, id, applied_at, statement_count) VALUES (?, ?, ?, ?, ?)`,
2301
+ params: [
2302
+ migrationKey(MIGRATIONS_META_NAMESPACE, MIGRATIONS_META_ID),
2303
+ MIGRATIONS_META_NAMESPACE,
2304
+ MIGRATIONS_META_ID,
2305
+ (/* @__PURE__ */ new Date()).toISOString(),
2306
+ 1
2307
+ ]
2308
+ }
2309
+ ],
2310
+ { signal }
2311
+ );
2312
+ if (!result.ok) return result;
2313
+ return ok(void 0);
2314
+ }
1890
2315
  // === Private helpers ===
1891
- async invokeSQL(dbName, action, body, signal) {
2316
+ async invokeSQL(dbName, actions, body, signal) {
1892
2317
  const session = this.context.session;
1893
- const headers = this.context.invoke(session, "sql", dbName, action);
2318
+ const actionList = Array.isArray(actions) ? actions : [actions];
2319
+ const headers = actionList.length === 1 ? this.context.invoke(session, "sql", dbName, actionList[0]) : this.invokeSQLAny(session, dbName, actionList);
1894
2320
  return this.context.fetch(`${this.host}/invoke`, {
1895
2321
  method: "POST",
1896
2322
  headers: {
@@ -1902,26 +2328,50 @@ var SQLService = class extends BaseService {
1902
2328
  });
1903
2329
  }
1904
2330
  actionForSql(sql, fallback) {
1905
- return firstSqlToken(sql) === "pragma" ? SQLAction.ADMIN : fallback;
2331
+ const token = firstSqlToken(sql);
2332
+ if (token === "pragma") return SQLAction.ADMIN;
2333
+ if (token !== void 0 && DDL_TOKENS.has(token)) return SQLAction.SCHEMA;
2334
+ return fallback;
2335
+ }
2336
+ actionsForSqlBatch(statements) {
2337
+ return this.dedupeActions(
2338
+ statements.map((statement) => this.actionForSql(statement.sql, SQLAction.WRITE))
2339
+ );
1906
2340
  }
1907
- actionForSqlBatch(statements) {
1908
- return statements.some(
1909
- (statement) => this.actionForSql(statement.sql, SQLAction.WRITE) === SQLAction.ADMIN
1910
- ) ? SQLAction.ADMIN : SQLAction.WRITE;
2341
+ dedupeActions(actions) {
2342
+ return [...new Set(actions)];
2343
+ }
2344
+ invokeSQLAny(session, dbName, actions) {
2345
+ if (!this.context.invokeAny) {
2346
+ throw new Error(
2347
+ `SQL operation requires multiple permissions (${actions.join(", ")}) but this SDK runtime does not support multi-resource invocations`
2348
+ );
2349
+ }
2350
+ return this.context.invokeAny(
2351
+ session,
2352
+ actions.map((action) => ({
2353
+ spaceId: session.spaceId,
2354
+ service: "sql",
2355
+ path: dbName,
2356
+ action
2357
+ }))
2358
+ );
1911
2359
  }
1912
2360
  async handleErrorResponse(response, operation) {
1913
2361
  const errorText = await response.text();
1914
- let errorBody = {};
1915
- try {
1916
- errorBody = JSON.parse(errorText);
1917
- } catch {
1918
- }
2362
+ const errorBody = parseServiceErrorBody(errorText);
1919
2363
  const errorCode = this.mapHttpStatusToErrorCode(
1920
2364
  response.status,
1921
2365
  errorBody.error
1922
2366
  );
1923
- const message = errorBody.message || `SQL ${operation} failed: ${response.status} - ${errorText}`;
1924
- const meta = { status: response.status, statusText: response.statusText };
2367
+ const message = formatServiceResponseError(
2368
+ "SQL",
2369
+ operation,
2370
+ response.status,
2371
+ errorText,
2372
+ errorBody
2373
+ );
2374
+ const meta = responseErrorMeta(response.status, response.statusText, errorText);
1925
2375
  if (response.status === 401) {
1926
2376
  const { resource, action } = parseAuthError(errorText);
1927
2377
  if (action) meta.requiredAction = action;
@@ -1981,6 +2431,53 @@ function firstSqlToken(sql) {
1981
2431
  const match = /^[A-Za-z_]+/.exec(sql.slice(index));
1982
2432
  return match?.[0].toLowerCase();
1983
2433
  }
2434
+ function validateMigrationOptions(options) {
2435
+ if (!options || typeof options !== "object") {
2436
+ return "SQL migrations require options";
2437
+ }
2438
+ if (!options.namespace || typeof options.namespace !== "string") {
2439
+ return "SQL migrations require a non-empty namespace";
2440
+ }
2441
+ if (!Array.isArray(options.migrations)) {
2442
+ return "SQL migrations require an ordered migrations array";
2443
+ }
2444
+ const ids = /* @__PURE__ */ new Set();
2445
+ for (const migration of options.migrations) {
2446
+ if (!migration.id || typeof migration.id !== "string") {
2447
+ return "SQL migrations require every migration to have a non-empty id";
2448
+ }
2449
+ if (ids.has(migration.id)) {
2450
+ return `Duplicate SQL migration id: ${migration.id}`;
2451
+ }
2452
+ ids.add(migration.id);
2453
+ if (!Array.isArray(migration.sql)) {
2454
+ return `SQL migration ${migration.id} requires a sql array`;
2455
+ }
2456
+ for (const statement of migration.sql) {
2457
+ if (typeof statement === "string") {
2458
+ if (statement.trim() === "") {
2459
+ return `SQL migration ${migration.id} contains an empty statement`;
2460
+ }
2461
+ continue;
2462
+ }
2463
+ if (!statement || typeof statement.sql !== "string" || statement.sql.trim() === "") {
2464
+ return `SQL migration ${migration.id} contains an invalid statement`;
2465
+ }
2466
+ }
2467
+ }
2468
+ return null;
2469
+ }
2470
+ function migrationKey(namespace, id) {
2471
+ return `${encodeURIComponent(namespace)}:${encodeURIComponent(id)}`;
2472
+ }
2473
+ function rowValue(row, index) {
2474
+ if (Array.isArray(row)) return row[index];
2475
+ if (row && typeof row === "object") {
2476
+ const values = Object.values(row);
2477
+ return values[index];
2478
+ }
2479
+ return void 0;
2480
+ }
1984
2481
 
1985
2482
  // src/duckdb/DuckDbDatabaseHandle.ts
1986
2483
  var DuckDbDatabaseHandle = class {
@@ -2290,17 +2787,19 @@ var DuckDbService = class extends BaseService {
2290
2787
  }
2291
2788
  async handleErrorResponse(response, operation) {
2292
2789
  const errorText = await response.text();
2293
- let errorBody = {};
2294
- try {
2295
- errorBody = JSON.parse(errorText);
2296
- } catch {
2297
- }
2790
+ const errorBody = parseServiceErrorBody(errorText);
2298
2791
  const errorCode = this.mapHttpStatusToErrorCode(
2299
2792
  response.status,
2300
2793
  errorBody.error
2301
2794
  );
2302
- const message = errorBody.message || `DuckDB ${operation} failed: ${response.status} - ${errorText}`;
2303
- const meta = { status: response.status, statusText: response.statusText };
2795
+ const message = formatServiceResponseError(
2796
+ "DuckDB",
2797
+ operation,
2798
+ response.status,
2799
+ errorText,
2800
+ errorBody
2801
+ );
2802
+ const meta = responseErrorMeta(response.status, response.statusText, errorText);
2304
2803
  if (response.status === 401) {
2305
2804
  const { resource, action } = parseAuthError(errorText);
2306
2805
  if (action) meta.requiredAction = action;
@@ -5648,6 +6147,7 @@ export {
5648
6147
  RetryPolicySchema,
5649
6148
  SECRET_NAME_RE,
5650
6149
  SQLAction,
6150
+ SQLMigrations,
5651
6151
  SQLService,
5652
6152
  SecretsService,
5653
6153
  ServiceContext,
@@ -5659,6 +6159,7 @@ export {
5659
6159
  ServiceSessionSchema,
5660
6160
  TelemetryEvents,
5661
6161
  TelemetrySpanEventSchema,
6162
+ TinyCloudDebugLogger,
5662
6163
  TinyCloudQuota,
5663
6164
  VaultHeaders,
5664
6165
  VaultPublicSpaceKVActions,
@@ -5678,21 +6179,26 @@ export {
5678
6179
  canonicalize as canonicalizeEncryptionJson,
5679
6180
  canonicalizeSecretScope,
5680
6181
  checkDecryptInvocationInput,
6182
+ clearTinyCloudDebugLogs,
5681
6183
  createKVResponseSchema,
5682
6184
  createResultSchema,
5683
6185
  createVaultCrypto,
5684
6186
  decryptEnvelopeWithKey,
5685
6187
  defaultRetryPolicy,
5686
6188
  deriveSignedReceiverKey,
6189
+ disableTinyCloudDebug,
5687
6190
  discoverNetwork,
6191
+ enableTinyCloudDebug,
5688
6192
  encryptToNetwork,
5689
6193
  encryptionError,
5690
6194
  ensureNetworkUsableForDecrypt,
5691
6195
  err,
5692
6196
  errorResult,
5693
6197
  generateRandomReceiverKey,
6198
+ getTinyCloudDebugLogs,
5694
6199
  hexDecode,
5695
6200
  hexEncode2 as hexEncode,
6201
+ installTinyCloudDebugGlobals,
5696
6202
  isNetworkId,
5697
6203
  networkDiscoveryKey,
5698
6204
  networkError,
@@ -5708,6 +6214,7 @@ export {
5708
6214
  storageLimitReachedError,
5709
6215
  storageQuotaExceededError,
5710
6216
  timeoutError,
6217
+ tinyCloudDebugLogger,
5711
6218
  utf8Decode,
5712
6219
  utf8Encode,
5713
6220
  validateEnvelope,