@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.cjs +541 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +75 -26
- package/dist/index.d.ts +75 -26
- package/dist/index.js +533 -26
- package/dist/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.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.
|
|
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.
|
|
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,
|
|
2316
|
+
async invokeSQL(dbName, actions, body, signal) {
|
|
1892
2317
|
const session = this.context.session;
|
|
1893
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
1908
|
-
return
|
|
1909
|
-
|
|
1910
|
-
|
|
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
|
-
|
|
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 =
|
|
1924
|
-
|
|
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
|
-
|
|
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 =
|
|
2303
|
-
|
|
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,
|