@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.cjs
CHANGED
|
@@ -52,6 +52,7 @@ __export(index_exports, {
|
|
|
52
52
|
RetryPolicySchema: () => RetryPolicySchema,
|
|
53
53
|
SECRET_NAME_RE: () => SECRET_NAME_RE,
|
|
54
54
|
SQLAction: () => SQLAction,
|
|
55
|
+
SQLMigrations: () => SQLMigrations,
|
|
55
56
|
SQLService: () => SQLService,
|
|
56
57
|
SecretsService: () => SecretsService,
|
|
57
58
|
ServiceContext: () => ServiceContext,
|
|
@@ -63,6 +64,7 @@ __export(index_exports, {
|
|
|
63
64
|
ServiceSessionSchema: () => ServiceSessionSchema,
|
|
64
65
|
TelemetryEvents: () => TelemetryEvents,
|
|
65
66
|
TelemetrySpanEventSchema: () => TelemetrySpanEventSchema,
|
|
67
|
+
TinyCloudDebugLogger: () => TinyCloudDebugLogger,
|
|
66
68
|
TinyCloudQuota: () => TinyCloudQuota,
|
|
67
69
|
VaultHeaders: () => VaultHeaders,
|
|
68
70
|
VaultPublicSpaceKVActions: () => VaultPublicSpaceKVActions,
|
|
@@ -82,21 +84,26 @@ __export(index_exports, {
|
|
|
82
84
|
canonicalizeEncryptionJson: () => canonicalize,
|
|
83
85
|
canonicalizeSecretScope: () => canonicalizeSecretScope,
|
|
84
86
|
checkDecryptInvocationInput: () => checkDecryptInvocationInput,
|
|
87
|
+
clearTinyCloudDebugLogs: () => clearTinyCloudDebugLogs,
|
|
85
88
|
createKVResponseSchema: () => createKVResponseSchema,
|
|
86
89
|
createResultSchema: () => createResultSchema,
|
|
87
90
|
createVaultCrypto: () => createVaultCrypto,
|
|
88
91
|
decryptEnvelopeWithKey: () => decryptEnvelopeWithKey,
|
|
89
92
|
defaultRetryPolicy: () => defaultRetryPolicy,
|
|
90
93
|
deriveSignedReceiverKey: () => deriveSignedReceiverKey,
|
|
94
|
+
disableTinyCloudDebug: () => disableTinyCloudDebug,
|
|
91
95
|
discoverNetwork: () => discoverNetwork,
|
|
96
|
+
enableTinyCloudDebug: () => enableTinyCloudDebug,
|
|
92
97
|
encryptToNetwork: () => encryptToNetwork,
|
|
93
98
|
encryptionError: () => encryptionError,
|
|
94
99
|
ensureNetworkUsableForDecrypt: () => ensureNetworkUsableForDecrypt,
|
|
95
100
|
err: () => err,
|
|
96
101
|
errorResult: () => errorResult,
|
|
97
102
|
generateRandomReceiverKey: () => generateRandomReceiverKey,
|
|
103
|
+
getTinyCloudDebugLogs: () => getTinyCloudDebugLogs,
|
|
98
104
|
hexDecode: () => hexDecode,
|
|
99
105
|
hexEncode: () => hexEncode2,
|
|
106
|
+
installTinyCloudDebugGlobals: () => installTinyCloudDebugGlobals,
|
|
100
107
|
isNetworkId: () => isNetworkId,
|
|
101
108
|
networkDiscoveryKey: () => networkDiscoveryKey,
|
|
102
109
|
networkError: () => networkError,
|
|
@@ -112,6 +119,7 @@ __export(index_exports, {
|
|
|
112
119
|
storageLimitReachedError: () => storageLimitReachedError,
|
|
113
120
|
storageQuotaExceededError: () => storageQuotaExceededError,
|
|
114
121
|
timeoutError: () => timeoutError,
|
|
122
|
+
tinyCloudDebugLogger: () => tinyCloudDebugLogger,
|
|
115
123
|
utf8Decode: () => utf8Decode,
|
|
116
124
|
utf8Encode: () => utf8Encode,
|
|
117
125
|
validateEnvelope: () => validateEnvelope,
|
|
@@ -197,6 +205,202 @@ function serviceError(code, message, service, options) {
|
|
|
197
205
|
};
|
|
198
206
|
}
|
|
199
207
|
|
|
208
|
+
// src/debug.ts
|
|
209
|
+
var DEBUG_FLAG = "TinyCloud_debug";
|
|
210
|
+
var MAX_EVENTS = 1e3;
|
|
211
|
+
function getGlobal() {
|
|
212
|
+
return globalThis;
|
|
213
|
+
}
|
|
214
|
+
function nowMs() {
|
|
215
|
+
const performanceNow = globalThis.performance?.now?.bind(globalThis.performance);
|
|
216
|
+
return typeof performanceNow === "function" ? performanceNow() : Date.now();
|
|
217
|
+
}
|
|
218
|
+
function isTrue(value) {
|
|
219
|
+
return value === true || value === "true" || value === "1";
|
|
220
|
+
}
|
|
221
|
+
function getProcessDebugFlag() {
|
|
222
|
+
const processLike = globalThis.process;
|
|
223
|
+
return processLike?.env?.[DEBUG_FLAG];
|
|
224
|
+
}
|
|
225
|
+
function isBrowserWindow() {
|
|
226
|
+
const global = getGlobal();
|
|
227
|
+
return global.window === globalThis;
|
|
228
|
+
}
|
|
229
|
+
function getLocalStorage() {
|
|
230
|
+
if (!isBrowserWindow()) {
|
|
231
|
+
return void 0;
|
|
232
|
+
}
|
|
233
|
+
try {
|
|
234
|
+
return globalThis.localStorage;
|
|
235
|
+
} catch {
|
|
236
|
+
return void 0;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
function getStoredDebugFlag() {
|
|
240
|
+
const storage = getLocalStorage();
|
|
241
|
+
if (!storage) {
|
|
242
|
+
return void 0;
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
return storage.getItem(DEBUG_FLAG);
|
|
246
|
+
} catch {
|
|
247
|
+
return void 0;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function setStoredDebugFlag(enabled) {
|
|
251
|
+
const storage = getLocalStorage();
|
|
252
|
+
if (!storage) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
if (enabled) {
|
|
257
|
+
storage.setItem(DEBUG_FLAG, "true");
|
|
258
|
+
} else {
|
|
259
|
+
storage.removeItem(DEBUG_FLAG);
|
|
260
|
+
}
|
|
261
|
+
} catch {
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function shouldStartEnabled() {
|
|
265
|
+
const global = getGlobal();
|
|
266
|
+
return isTrue(global[DEBUG_FLAG]) || isTrue(getStoredDebugFlag()) || isTrue(getProcessDebugFlag());
|
|
267
|
+
}
|
|
268
|
+
var TinyCloudDebugLogger = class {
|
|
269
|
+
constructor() {
|
|
270
|
+
this.enabled = shouldStartEnabled();
|
|
271
|
+
this.sequence = 0;
|
|
272
|
+
this.events = [];
|
|
273
|
+
this.maxEvents = MAX_EVENTS;
|
|
274
|
+
}
|
|
275
|
+
isEnabled() {
|
|
276
|
+
return this.enabled || isTrue(getGlobal()[DEBUG_FLAG]) || isTrue(getProcessDebugFlag());
|
|
277
|
+
}
|
|
278
|
+
enable(options = {}) {
|
|
279
|
+
this.enabled = true;
|
|
280
|
+
getGlobal()[DEBUG_FLAG] = true;
|
|
281
|
+
if (options.persist !== false) {
|
|
282
|
+
setStoredDebugFlag(true);
|
|
283
|
+
}
|
|
284
|
+
this.log("debug.enabled", { persisted: options.persist !== false });
|
|
285
|
+
}
|
|
286
|
+
disable(options = {}) {
|
|
287
|
+
this.log("debug.disabled", { persisted: options.persist !== false });
|
|
288
|
+
this.enabled = false;
|
|
289
|
+
getGlobal()[DEBUG_FLAG] = false;
|
|
290
|
+
if (options.persist !== false) {
|
|
291
|
+
setStoredDebugFlag(false);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
clear() {
|
|
295
|
+
this.events = [];
|
|
296
|
+
}
|
|
297
|
+
getLogs() {
|
|
298
|
+
return [...this.events];
|
|
299
|
+
}
|
|
300
|
+
log(event, data, message) {
|
|
301
|
+
if (!this.isEnabled()) {
|
|
302
|
+
return void 0;
|
|
303
|
+
}
|
|
304
|
+
return this.record({
|
|
305
|
+
event,
|
|
306
|
+
data,
|
|
307
|
+
message,
|
|
308
|
+
level: "debug",
|
|
309
|
+
timestamp: Date.now(),
|
|
310
|
+
timestampIso: (/* @__PURE__ */ new Date()).toISOString()
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
startTimer(event, data) {
|
|
314
|
+
if (!this.isEnabled()) {
|
|
315
|
+
return { stop: () => void 0 };
|
|
316
|
+
}
|
|
317
|
+
const startedAt = nowMs();
|
|
318
|
+
this.log(`${event}.start`, data);
|
|
319
|
+
return {
|
|
320
|
+
stop: (finishData) => {
|
|
321
|
+
const endedAt = nowMs();
|
|
322
|
+
return this.record({
|
|
323
|
+
event: `${event}.end`,
|
|
324
|
+
data: finishData,
|
|
325
|
+
durationMs: endedAt - startedAt,
|
|
326
|
+
startedAt,
|
|
327
|
+
endedAt,
|
|
328
|
+
level: "debug",
|
|
329
|
+
timestamp: Date.now(),
|
|
330
|
+
timestampIso: (/* @__PURE__ */ new Date()).toISOString()
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
async timeAsync(event, operation, data) {
|
|
336
|
+
if (!this.isEnabled()) {
|
|
337
|
+
return operation();
|
|
338
|
+
}
|
|
339
|
+
const timer = this.startTimer(event, data);
|
|
340
|
+
try {
|
|
341
|
+
const result = await operation();
|
|
342
|
+
timer.stop({ ok: true });
|
|
343
|
+
return result;
|
|
344
|
+
} catch (error) {
|
|
345
|
+
timer.stop({ ok: false, error });
|
|
346
|
+
throw error;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
time(event, operation, data) {
|
|
350
|
+
if (!this.isEnabled()) {
|
|
351
|
+
return operation();
|
|
352
|
+
}
|
|
353
|
+
const timer = this.startTimer(event, data);
|
|
354
|
+
try {
|
|
355
|
+
const result = operation();
|
|
356
|
+
timer.stop({ ok: true });
|
|
357
|
+
return result;
|
|
358
|
+
} catch (error) {
|
|
359
|
+
timer.stop({ ok: false, error });
|
|
360
|
+
throw error;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
record(event) {
|
|
364
|
+
const debugEvent = {
|
|
365
|
+
...event,
|
|
366
|
+
sequence: ++this.sequence
|
|
367
|
+
};
|
|
368
|
+
this.events.push(debugEvent);
|
|
369
|
+
if (this.events.length > this.maxEvents) {
|
|
370
|
+
this.events.splice(0, this.events.length - this.maxEvents);
|
|
371
|
+
}
|
|
372
|
+
try {
|
|
373
|
+
globalThis.console?.debug?.("[TinyCloud]", debugEvent.event, debugEvent);
|
|
374
|
+
} catch {
|
|
375
|
+
}
|
|
376
|
+
return debugEvent;
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
var tinyCloudDebugLogger = new TinyCloudDebugLogger();
|
|
380
|
+
function enableTinyCloudDebug(options) {
|
|
381
|
+
tinyCloudDebugLogger.enable(options);
|
|
382
|
+
return tinyCloudDebugLogger;
|
|
383
|
+
}
|
|
384
|
+
function disableTinyCloudDebug(options) {
|
|
385
|
+
tinyCloudDebugLogger.disable(options);
|
|
386
|
+
return tinyCloudDebugLogger;
|
|
387
|
+
}
|
|
388
|
+
function getTinyCloudDebugLogs() {
|
|
389
|
+
return tinyCloudDebugLogger.getLogs();
|
|
390
|
+
}
|
|
391
|
+
function clearTinyCloudDebugLogs() {
|
|
392
|
+
tinyCloudDebugLogger.clear();
|
|
393
|
+
}
|
|
394
|
+
function installTinyCloudDebugGlobals() {
|
|
395
|
+
const global = getGlobal();
|
|
396
|
+
global.TinyCloudDebug = tinyCloudDebugLogger;
|
|
397
|
+
global.enableTinyCloudDebug = enableTinyCloudDebug;
|
|
398
|
+
global.disableTinyCloudDebug = disableTinyCloudDebug;
|
|
399
|
+
global.getTinyCloudDebugLogs = getTinyCloudDebugLogs;
|
|
400
|
+
global.clearTinyCloudDebugLogs = clearTinyCloudDebugLogs;
|
|
401
|
+
}
|
|
402
|
+
installTinyCloudDebugGlobals();
|
|
403
|
+
|
|
200
404
|
// src/types.schema.ts
|
|
201
405
|
var import_zod = require("zod");
|
|
202
406
|
var ServiceErrorSchema = import_zod.z.object({
|
|
@@ -438,9 +642,9 @@ var ServiceContext = class {
|
|
|
438
642
|
this._services = /* @__PURE__ */ new Map();
|
|
439
643
|
this._eventHandlers = /* @__PURE__ */ new Map();
|
|
440
644
|
this._abortController = new AbortController();
|
|
441
|
-
this._invoke = config.invoke;
|
|
442
|
-
this._invokeAny = config.invokeAny;
|
|
443
|
-
this._fetch = config.fetch ?? globalThis.fetch.bind(globalThis);
|
|
645
|
+
this._invoke = this.wrapInvoke(config.invoke);
|
|
646
|
+
this._invokeAny = config.invokeAny ? this.wrapInvokeAny(config.invokeAny) : void 0;
|
|
647
|
+
this._fetch = this.wrapFetch(config.fetch ?? globalThis.fetch.bind(globalThis));
|
|
444
648
|
this._hosts = config.hosts;
|
|
445
649
|
this._session = config.session ?? null;
|
|
446
650
|
this._retryPolicy = {
|
|
@@ -543,6 +747,7 @@ var ServiceContext = class {
|
|
|
543
747
|
* @param data - Event data
|
|
544
748
|
*/
|
|
545
749
|
emit(event, data) {
|
|
750
|
+
tinyCloudDebugLogger.log(event, data);
|
|
546
751
|
if (this._telemetryEnabled && this._telemetryHandler) {
|
|
547
752
|
try {
|
|
548
753
|
this._telemetryHandler(event, data);
|
|
@@ -632,6 +837,77 @@ var ServiceContext = class {
|
|
|
632
837
|
get retryPolicy() {
|
|
633
838
|
return this._retryPolicy;
|
|
634
839
|
}
|
|
840
|
+
wrapInvoke(invoke) {
|
|
841
|
+
return (session, service, path, action, facts) => {
|
|
842
|
+
if (!tinyCloudDebugLogger.isEnabled()) {
|
|
843
|
+
return invoke(session, service, path, action, facts);
|
|
844
|
+
}
|
|
845
|
+
const timer = tinyCloudDebugLogger.startTimer("sdk.invoke", {
|
|
846
|
+
service,
|
|
847
|
+
path,
|
|
848
|
+
action,
|
|
849
|
+
facts
|
|
850
|
+
});
|
|
851
|
+
try {
|
|
852
|
+
const result = invoke(session, service, path, action, facts);
|
|
853
|
+
timer.stop({ ok: true, service, path, action });
|
|
854
|
+
return result;
|
|
855
|
+
} catch (error) {
|
|
856
|
+
timer.stop({ ok: false, service, path, action, error });
|
|
857
|
+
throw error;
|
|
858
|
+
}
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
wrapInvokeAny(invokeAny) {
|
|
862
|
+
return (session, entries, facts) => {
|
|
863
|
+
if (!tinyCloudDebugLogger.isEnabled()) {
|
|
864
|
+
return invokeAny(session, entries, facts);
|
|
865
|
+
}
|
|
866
|
+
const timer = tinyCloudDebugLogger.startTimer("sdk.invokeAny", {
|
|
867
|
+
entries,
|
|
868
|
+
facts
|
|
869
|
+
});
|
|
870
|
+
try {
|
|
871
|
+
const result = invokeAny(session, entries, facts);
|
|
872
|
+
timer.stop({ ok: true, entries });
|
|
873
|
+
return result;
|
|
874
|
+
} catch (error) {
|
|
875
|
+
timer.stop({ ok: false, entries, error });
|
|
876
|
+
throw error;
|
|
877
|
+
}
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
wrapFetch(fetchFn) {
|
|
881
|
+
return async (url, init) => {
|
|
882
|
+
if (!tinyCloudDebugLogger.isEnabled()) {
|
|
883
|
+
return fetchFn(url, init);
|
|
884
|
+
}
|
|
885
|
+
const timer = tinyCloudDebugLogger.startTimer("sdk.fetch", {
|
|
886
|
+
url,
|
|
887
|
+
method: init?.method ?? "GET",
|
|
888
|
+
init
|
|
889
|
+
});
|
|
890
|
+
try {
|
|
891
|
+
const response = await fetchFn(url, init);
|
|
892
|
+
timer.stop({
|
|
893
|
+
ok: response.ok,
|
|
894
|
+
url,
|
|
895
|
+
method: init?.method ?? "GET",
|
|
896
|
+
status: response.status,
|
|
897
|
+
statusText: response.statusText
|
|
898
|
+
});
|
|
899
|
+
return response;
|
|
900
|
+
} catch (error) {
|
|
901
|
+
timer.stop({
|
|
902
|
+
ok: false,
|
|
903
|
+
url,
|
|
904
|
+
method: init?.method ?? "GET",
|
|
905
|
+
error
|
|
906
|
+
});
|
|
907
|
+
throw error;
|
|
908
|
+
}
|
|
909
|
+
};
|
|
910
|
+
}
|
|
635
911
|
};
|
|
636
912
|
|
|
637
913
|
// src/errors.ts
|
|
@@ -1817,11 +2093,59 @@ var KVService = class extends BaseService {
|
|
|
1817
2093
|
*/
|
|
1818
2094
|
KVService.serviceName = "kv";
|
|
1819
2095
|
|
|
2096
|
+
// src/responseErrors.ts
|
|
2097
|
+
function parseServiceErrorBody(errorText) {
|
|
2098
|
+
try {
|
|
2099
|
+
return JSON.parse(errorText);
|
|
2100
|
+
} catch {
|
|
2101
|
+
return {};
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
function formatServiceResponseError(serviceLabel, operation, status, errorText, parsed) {
|
|
2105
|
+
if (parsed.message) {
|
|
2106
|
+
return compactMessage(parsed.message);
|
|
2107
|
+
}
|
|
2108
|
+
if (looksLikeHtml(errorText)) {
|
|
2109
|
+
if (status === 524 || /524\s*[:-]/i.test(errorText) || /a timeout occurred/i.test(errorText)) {
|
|
2110
|
+
return `${serviceLabel} ${operation} failed: upstream request timed out. Please retry.`;
|
|
2111
|
+
}
|
|
2112
|
+
return `${serviceLabel} ${operation} failed: upstream service returned an HTML error page (${status}).`;
|
|
2113
|
+
}
|
|
2114
|
+
return `${serviceLabel} ${operation} failed: ${status} - ${compactMessage(errorText)}`;
|
|
2115
|
+
}
|
|
2116
|
+
function responseErrorMeta(status, statusText, errorText) {
|
|
2117
|
+
const meta = { status, statusText };
|
|
2118
|
+
const snippet = compactMessage(errorText);
|
|
2119
|
+
if (snippet) {
|
|
2120
|
+
meta.responseSnippet = snippet.slice(0, 300);
|
|
2121
|
+
}
|
|
2122
|
+
return meta;
|
|
2123
|
+
}
|
|
2124
|
+
function looksLikeHtml(text) {
|
|
2125
|
+
return /<!doctype html/i.test(text) || /<html[\s>]/i.test(text);
|
|
2126
|
+
}
|
|
2127
|
+
function compactMessage(message) {
|
|
2128
|
+
const compact = message.replace(/\s+/g, " ").trim();
|
|
2129
|
+
return compact.length > 500 ? `${compact.slice(0, 500)}...` : compact;
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
// src/sql/SQLMigrations.ts
|
|
2133
|
+
var SQLMigrations = class {
|
|
2134
|
+
constructor(service, dbName) {
|
|
2135
|
+
this.service = service;
|
|
2136
|
+
this.dbName = dbName;
|
|
2137
|
+
}
|
|
2138
|
+
apply(options) {
|
|
2139
|
+
return this.service.applyMigrationsOnDb(this.dbName, options);
|
|
2140
|
+
}
|
|
2141
|
+
};
|
|
2142
|
+
|
|
1820
2143
|
// src/sql/DatabaseHandle.ts
|
|
1821
2144
|
var DatabaseHandle = class {
|
|
1822
2145
|
constructor(service, name) {
|
|
1823
2146
|
this.service = service;
|
|
1824
2147
|
this.name = name;
|
|
2148
|
+
this.migrations = new SQLMigrations(service, name);
|
|
1825
2149
|
}
|
|
1826
2150
|
async query(sql, params, options) {
|
|
1827
2151
|
return this.service.queryOnDb(this.name, sql, params, options);
|
|
@@ -1844,6 +2168,7 @@ var DatabaseHandle = class {
|
|
|
1844
2168
|
var SQLAction = {
|
|
1845
2169
|
READ: "tinycloud.sql/read",
|
|
1846
2170
|
WRITE: "tinycloud.sql/write",
|
|
2171
|
+
SCHEMA: "tinycloud.sql/schema",
|
|
1847
2172
|
ADMIN: "tinycloud.sql/admin",
|
|
1848
2173
|
SELECT: "tinycloud.sql/select",
|
|
1849
2174
|
INSERT: "tinycloud.sql/insert",
|
|
@@ -1855,9 +2180,21 @@ var SQLAction = {
|
|
|
1855
2180
|
};
|
|
1856
2181
|
|
|
1857
2182
|
// src/sql/SQLService.ts
|
|
2183
|
+
var DDL_TOKENS = /* @__PURE__ */ new Set(["alter", "create", "drop"]);
|
|
2184
|
+
var MIGRATIONS_TABLE = "__tinycloud_sql_migrations";
|
|
2185
|
+
var MIGRATIONS_SCHEMA = `CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (
|
|
2186
|
+
key TEXT PRIMARY KEY,
|
|
2187
|
+
namespace TEXT NOT NULL,
|
|
2188
|
+
id TEXT NOT NULL,
|
|
2189
|
+
applied_at TEXT NOT NULL,
|
|
2190
|
+
statement_count INTEGER NOT NULL
|
|
2191
|
+
)`;
|
|
2192
|
+
var MIGRATIONS_META_NAMESPACE = "tinycloud.sql.migrations";
|
|
2193
|
+
var MIGRATIONS_META_ID = "000_metadata";
|
|
1858
2194
|
var SQLService = class extends BaseService {
|
|
1859
2195
|
constructor(config = {}) {
|
|
1860
2196
|
super();
|
|
2197
|
+
this.migrationLocks = /* @__PURE__ */ new Map();
|
|
1861
2198
|
this._config = config;
|
|
1862
2199
|
}
|
|
1863
2200
|
get config() {
|
|
@@ -1893,6 +2230,24 @@ var SQLService = class extends BaseService {
|
|
|
1893
2230
|
async batch(statements, options) {
|
|
1894
2231
|
return this.batchOnDb(this.defaultDbName, statements, options);
|
|
1895
2232
|
}
|
|
2233
|
+
async applyMigrationsOnDb(dbName, options) {
|
|
2234
|
+
const validationError = validateMigrationOptions(options);
|
|
2235
|
+
if (validationError) {
|
|
2236
|
+
return err(serviceError(ErrorCodes.INVALID_INPUT, validationError, "sql"));
|
|
2237
|
+
}
|
|
2238
|
+
const lockKey = `${dbName}\0${options.namespace}`;
|
|
2239
|
+
const existing = this.migrationLocks.get(lockKey);
|
|
2240
|
+
if (existing) {
|
|
2241
|
+
return existing;
|
|
2242
|
+
}
|
|
2243
|
+
const promise = this.applyMigrationsOnDbUnlocked(dbName, options);
|
|
2244
|
+
this.migrationLocks.set(lockKey, promise);
|
|
2245
|
+
try {
|
|
2246
|
+
return await promise;
|
|
2247
|
+
} finally {
|
|
2248
|
+
this.migrationLocks.delete(lockKey);
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
1896
2251
|
// === Internal methods called by DatabaseHandle ===
|
|
1897
2252
|
async queryOnDb(dbName, sql, params, options) {
|
|
1898
2253
|
return this.withTelemetry("query", dbName, async () => {
|
|
@@ -1930,9 +2285,15 @@ var SQLService = class extends BaseService {
|
|
|
1930
2285
|
if (options?.schema) {
|
|
1931
2286
|
body.schema = options.schema;
|
|
1932
2287
|
}
|
|
2288
|
+
const actions = [
|
|
2289
|
+
this.actionForSql(sql, SQLAction.WRITE),
|
|
2290
|
+
...(options?.schema ?? []).map(
|
|
2291
|
+
(statement) => this.actionForSql(statement, SQLAction.SCHEMA)
|
|
2292
|
+
)
|
|
2293
|
+
];
|
|
1933
2294
|
const response = await this.invokeSQL(
|
|
1934
2295
|
dbName,
|
|
1935
|
-
this.
|
|
2296
|
+
this.dedupeActions(actions),
|
|
1936
2297
|
body,
|
|
1937
2298
|
options?.signal
|
|
1938
2299
|
);
|
|
@@ -1954,7 +2315,7 @@ var SQLService = class extends BaseService {
|
|
|
1954
2315
|
try {
|
|
1955
2316
|
const response = await this.invokeSQL(
|
|
1956
2317
|
dbName,
|
|
1957
|
-
this.
|
|
2318
|
+
this.actionsForSqlBatch(statements),
|
|
1958
2319
|
{ action: "batch", statements },
|
|
1959
2320
|
options?.signal
|
|
1960
2321
|
);
|
|
@@ -2017,10 +2378,83 @@ var SQLService = class extends BaseService {
|
|
|
2017
2378
|
}
|
|
2018
2379
|
});
|
|
2019
2380
|
}
|
|
2381
|
+
async applyMigrationsOnDbUnlocked(dbName, options) {
|
|
2382
|
+
return this.withTelemetry("migrations.apply", dbName, async () => {
|
|
2383
|
+
const created = await this.ensureMigrationsTable(dbName, options.signal);
|
|
2384
|
+
if (!created.ok) return created;
|
|
2385
|
+
const listed = await this.queryOnDb(
|
|
2386
|
+
dbName,
|
|
2387
|
+
`SELECT id FROM ${MIGRATIONS_TABLE} WHERE namespace = ? ORDER BY applied_at, id`,
|
|
2388
|
+
[options.namespace],
|
|
2389
|
+
{ signal: options.signal }
|
|
2390
|
+
);
|
|
2391
|
+
if (!listed.ok) return listed;
|
|
2392
|
+
const appliedIds = new Set(
|
|
2393
|
+
listed.data.rows.map((row) => rowValue(row, 0)).filter((id) => typeof id === "string")
|
|
2394
|
+
);
|
|
2395
|
+
const skipped = options.migrations.filter((migration) => appliedIds.has(migration.id)).map((migration) => migration.id);
|
|
2396
|
+
const pending = options.migrations.filter((migration) => !appliedIds.has(migration.id));
|
|
2397
|
+
const applied = [];
|
|
2398
|
+
for (const migration of pending) {
|
|
2399
|
+
const result = await this.applyOneMigration(dbName, options.namespace, migration, options.signal);
|
|
2400
|
+
if (!result.ok) return result;
|
|
2401
|
+
applied.push(migration.id);
|
|
2402
|
+
}
|
|
2403
|
+
return ok({
|
|
2404
|
+
database: dbName,
|
|
2405
|
+
namespace: options.namespace,
|
|
2406
|
+
status: applied.length > 0 ? "applied" : "already_current",
|
|
2407
|
+
applied,
|
|
2408
|
+
skipped
|
|
2409
|
+
});
|
|
2410
|
+
});
|
|
2411
|
+
}
|
|
2412
|
+
async applyOneMigration(dbName, namespace, migration, signal) {
|
|
2413
|
+
const statements = [
|
|
2414
|
+
...migration.sql.map(
|
|
2415
|
+
(statement) => typeof statement === "string" ? { sql: statement } : statement
|
|
2416
|
+
),
|
|
2417
|
+
{
|
|
2418
|
+
sql: `INSERT OR REPLACE INTO ${MIGRATIONS_TABLE} (key, namespace, id, applied_at, statement_count) VALUES (?, ?, ?, ?, ?)`,
|
|
2419
|
+
params: [
|
|
2420
|
+
migrationKey(namespace, migration.id),
|
|
2421
|
+
namespace,
|
|
2422
|
+
migration.id,
|
|
2423
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
2424
|
+
migration.sql.length
|
|
2425
|
+
]
|
|
2426
|
+
}
|
|
2427
|
+
];
|
|
2428
|
+
const result = await this.batchOnDb(dbName, statements, { signal });
|
|
2429
|
+
if (!result.ok) return result;
|
|
2430
|
+
return ok(void 0);
|
|
2431
|
+
}
|
|
2432
|
+
async ensureMigrationsTable(dbName, signal) {
|
|
2433
|
+
const result = await this.batchOnDb(
|
|
2434
|
+
dbName,
|
|
2435
|
+
[
|
|
2436
|
+
{ sql: MIGRATIONS_SCHEMA },
|
|
2437
|
+
{
|
|
2438
|
+
sql: `INSERT OR REPLACE INTO ${MIGRATIONS_TABLE} (key, namespace, id, applied_at, statement_count) VALUES (?, ?, ?, ?, ?)`,
|
|
2439
|
+
params: [
|
|
2440
|
+
migrationKey(MIGRATIONS_META_NAMESPACE, MIGRATIONS_META_ID),
|
|
2441
|
+
MIGRATIONS_META_NAMESPACE,
|
|
2442
|
+
MIGRATIONS_META_ID,
|
|
2443
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
2444
|
+
1
|
|
2445
|
+
]
|
|
2446
|
+
}
|
|
2447
|
+
],
|
|
2448
|
+
{ signal }
|
|
2449
|
+
);
|
|
2450
|
+
if (!result.ok) return result;
|
|
2451
|
+
return ok(void 0);
|
|
2452
|
+
}
|
|
2020
2453
|
// === Private helpers ===
|
|
2021
|
-
async invokeSQL(dbName,
|
|
2454
|
+
async invokeSQL(dbName, actions, body, signal) {
|
|
2022
2455
|
const session = this.context.session;
|
|
2023
|
-
const
|
|
2456
|
+
const actionList = Array.isArray(actions) ? actions : [actions];
|
|
2457
|
+
const headers = actionList.length === 1 ? this.context.invoke(session, "sql", dbName, actionList[0]) : this.invokeSQLAny(session, dbName, actionList);
|
|
2024
2458
|
return this.context.fetch(`${this.host}/invoke`, {
|
|
2025
2459
|
method: "POST",
|
|
2026
2460
|
headers: {
|
|
@@ -2032,26 +2466,50 @@ var SQLService = class extends BaseService {
|
|
|
2032
2466
|
});
|
|
2033
2467
|
}
|
|
2034
2468
|
actionForSql(sql, fallback) {
|
|
2035
|
-
|
|
2469
|
+
const token = firstSqlToken(sql);
|
|
2470
|
+
if (token === "pragma") return SQLAction.ADMIN;
|
|
2471
|
+
if (token !== void 0 && DDL_TOKENS.has(token)) return SQLAction.SCHEMA;
|
|
2472
|
+
return fallback;
|
|
2473
|
+
}
|
|
2474
|
+
actionsForSqlBatch(statements) {
|
|
2475
|
+
return this.dedupeActions(
|
|
2476
|
+
statements.map((statement) => this.actionForSql(statement.sql, SQLAction.WRITE))
|
|
2477
|
+
);
|
|
2036
2478
|
}
|
|
2037
|
-
|
|
2038
|
-
return
|
|
2039
|
-
|
|
2040
|
-
|
|
2479
|
+
dedupeActions(actions) {
|
|
2480
|
+
return [...new Set(actions)];
|
|
2481
|
+
}
|
|
2482
|
+
invokeSQLAny(session, dbName, actions) {
|
|
2483
|
+
if (!this.context.invokeAny) {
|
|
2484
|
+
throw new Error(
|
|
2485
|
+
`SQL operation requires multiple permissions (${actions.join(", ")}) but this SDK runtime does not support multi-resource invocations`
|
|
2486
|
+
);
|
|
2487
|
+
}
|
|
2488
|
+
return this.context.invokeAny(
|
|
2489
|
+
session,
|
|
2490
|
+
actions.map((action) => ({
|
|
2491
|
+
spaceId: session.spaceId,
|
|
2492
|
+
service: "sql",
|
|
2493
|
+
path: dbName,
|
|
2494
|
+
action
|
|
2495
|
+
}))
|
|
2496
|
+
);
|
|
2041
2497
|
}
|
|
2042
2498
|
async handleErrorResponse(response, operation) {
|
|
2043
2499
|
const errorText = await response.text();
|
|
2044
|
-
|
|
2045
|
-
try {
|
|
2046
|
-
errorBody = JSON.parse(errorText);
|
|
2047
|
-
} catch {
|
|
2048
|
-
}
|
|
2500
|
+
const errorBody = parseServiceErrorBody(errorText);
|
|
2049
2501
|
const errorCode = this.mapHttpStatusToErrorCode(
|
|
2050
2502
|
response.status,
|
|
2051
2503
|
errorBody.error
|
|
2052
2504
|
);
|
|
2053
|
-
const message =
|
|
2054
|
-
|
|
2505
|
+
const message = formatServiceResponseError(
|
|
2506
|
+
"SQL",
|
|
2507
|
+
operation,
|
|
2508
|
+
response.status,
|
|
2509
|
+
errorText,
|
|
2510
|
+
errorBody
|
|
2511
|
+
);
|
|
2512
|
+
const meta = responseErrorMeta(response.status, response.statusText, errorText);
|
|
2055
2513
|
if (response.status === 401) {
|
|
2056
2514
|
const { resource, action } = parseAuthError(errorText);
|
|
2057
2515
|
if (action) meta.requiredAction = action;
|
|
@@ -2111,6 +2569,53 @@ function firstSqlToken(sql) {
|
|
|
2111
2569
|
const match = /^[A-Za-z_]+/.exec(sql.slice(index));
|
|
2112
2570
|
return match?.[0].toLowerCase();
|
|
2113
2571
|
}
|
|
2572
|
+
function validateMigrationOptions(options) {
|
|
2573
|
+
if (!options || typeof options !== "object") {
|
|
2574
|
+
return "SQL migrations require options";
|
|
2575
|
+
}
|
|
2576
|
+
if (!options.namespace || typeof options.namespace !== "string") {
|
|
2577
|
+
return "SQL migrations require a non-empty namespace";
|
|
2578
|
+
}
|
|
2579
|
+
if (!Array.isArray(options.migrations)) {
|
|
2580
|
+
return "SQL migrations require an ordered migrations array";
|
|
2581
|
+
}
|
|
2582
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2583
|
+
for (const migration of options.migrations) {
|
|
2584
|
+
if (!migration.id || typeof migration.id !== "string") {
|
|
2585
|
+
return "SQL migrations require every migration to have a non-empty id";
|
|
2586
|
+
}
|
|
2587
|
+
if (ids.has(migration.id)) {
|
|
2588
|
+
return `Duplicate SQL migration id: ${migration.id}`;
|
|
2589
|
+
}
|
|
2590
|
+
ids.add(migration.id);
|
|
2591
|
+
if (!Array.isArray(migration.sql)) {
|
|
2592
|
+
return `SQL migration ${migration.id} requires a sql array`;
|
|
2593
|
+
}
|
|
2594
|
+
for (const statement of migration.sql) {
|
|
2595
|
+
if (typeof statement === "string") {
|
|
2596
|
+
if (statement.trim() === "") {
|
|
2597
|
+
return `SQL migration ${migration.id} contains an empty statement`;
|
|
2598
|
+
}
|
|
2599
|
+
continue;
|
|
2600
|
+
}
|
|
2601
|
+
if (!statement || typeof statement.sql !== "string" || statement.sql.trim() === "") {
|
|
2602
|
+
return `SQL migration ${migration.id} contains an invalid statement`;
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
return null;
|
|
2607
|
+
}
|
|
2608
|
+
function migrationKey(namespace, id) {
|
|
2609
|
+
return `${encodeURIComponent(namespace)}:${encodeURIComponent(id)}`;
|
|
2610
|
+
}
|
|
2611
|
+
function rowValue(row, index) {
|
|
2612
|
+
if (Array.isArray(row)) return row[index];
|
|
2613
|
+
if (row && typeof row === "object") {
|
|
2614
|
+
const values = Object.values(row);
|
|
2615
|
+
return values[index];
|
|
2616
|
+
}
|
|
2617
|
+
return void 0;
|
|
2618
|
+
}
|
|
2114
2619
|
|
|
2115
2620
|
// src/duckdb/DuckDbDatabaseHandle.ts
|
|
2116
2621
|
var DuckDbDatabaseHandle = class {
|
|
@@ -2420,17 +2925,19 @@ var DuckDbService = class extends BaseService {
|
|
|
2420
2925
|
}
|
|
2421
2926
|
async handleErrorResponse(response, operation) {
|
|
2422
2927
|
const errorText = await response.text();
|
|
2423
|
-
|
|
2424
|
-
try {
|
|
2425
|
-
errorBody = JSON.parse(errorText);
|
|
2426
|
-
} catch {
|
|
2427
|
-
}
|
|
2928
|
+
const errorBody = parseServiceErrorBody(errorText);
|
|
2428
2929
|
const errorCode = this.mapHttpStatusToErrorCode(
|
|
2429
2930
|
response.status,
|
|
2430
2931
|
errorBody.error
|
|
2431
2932
|
);
|
|
2432
|
-
const message =
|
|
2433
|
-
|
|
2933
|
+
const message = formatServiceResponseError(
|
|
2934
|
+
"DuckDB",
|
|
2935
|
+
operation,
|
|
2936
|
+
response.status,
|
|
2937
|
+
errorText,
|
|
2938
|
+
errorBody
|
|
2939
|
+
);
|
|
2940
|
+
const meta = responseErrorMeta(response.status, response.statusText, errorText);
|
|
2434
2941
|
if (response.status === 401) {
|
|
2435
2942
|
const { resource, action } = parseAuthError(errorText);
|
|
2436
2943
|
if (action) meta.requiredAction = action;
|
|
@@ -5779,6 +6286,7 @@ EncryptionService.serviceName = "encryption";
|
|
|
5779
6286
|
RetryPolicySchema,
|
|
5780
6287
|
SECRET_NAME_RE,
|
|
5781
6288
|
SQLAction,
|
|
6289
|
+
SQLMigrations,
|
|
5782
6290
|
SQLService,
|
|
5783
6291
|
SecretsService,
|
|
5784
6292
|
ServiceContext,
|
|
@@ -5790,6 +6298,7 @@ EncryptionService.serviceName = "encryption";
|
|
|
5790
6298
|
ServiceSessionSchema,
|
|
5791
6299
|
TelemetryEvents,
|
|
5792
6300
|
TelemetrySpanEventSchema,
|
|
6301
|
+
TinyCloudDebugLogger,
|
|
5793
6302
|
TinyCloudQuota,
|
|
5794
6303
|
VaultHeaders,
|
|
5795
6304
|
VaultPublicSpaceKVActions,
|
|
@@ -5809,21 +6318,26 @@ EncryptionService.serviceName = "encryption";
|
|
|
5809
6318
|
canonicalizeEncryptionJson,
|
|
5810
6319
|
canonicalizeSecretScope,
|
|
5811
6320
|
checkDecryptInvocationInput,
|
|
6321
|
+
clearTinyCloudDebugLogs,
|
|
5812
6322
|
createKVResponseSchema,
|
|
5813
6323
|
createResultSchema,
|
|
5814
6324
|
createVaultCrypto,
|
|
5815
6325
|
decryptEnvelopeWithKey,
|
|
5816
6326
|
defaultRetryPolicy,
|
|
5817
6327
|
deriveSignedReceiverKey,
|
|
6328
|
+
disableTinyCloudDebug,
|
|
5818
6329
|
discoverNetwork,
|
|
6330
|
+
enableTinyCloudDebug,
|
|
5819
6331
|
encryptToNetwork,
|
|
5820
6332
|
encryptionError,
|
|
5821
6333
|
ensureNetworkUsableForDecrypt,
|
|
5822
6334
|
err,
|
|
5823
6335
|
errorResult,
|
|
5824
6336
|
generateRandomReceiverKey,
|
|
6337
|
+
getTinyCloudDebugLogs,
|
|
5825
6338
|
hexDecode,
|
|
5826
6339
|
hexEncode,
|
|
6340
|
+
installTinyCloudDebugGlobals,
|
|
5827
6341
|
isNetworkId,
|
|
5828
6342
|
networkDiscoveryKey,
|
|
5829
6343
|
networkError,
|
|
@@ -5839,6 +6353,7 @@ EncryptionService.serviceName = "encryption";
|
|
|
5839
6353
|
storageLimitReachedError,
|
|
5840
6354
|
storageQuotaExceededError,
|
|
5841
6355
|
timeoutError,
|
|
6356
|
+
tinyCloudDebugLogger,
|
|
5842
6357
|
utf8Decode,
|
|
5843
6358
|
utf8Encode,
|
|
5844
6359
|
validateEnvelope,
|