@tinycloud/sdk-services 2.4.0-beta.8 → 2.4.1
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 +504 -20
- 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 +496 -20
- package/dist/index.js.map +1 -1
- package/dist/sql/index.cjs +210 -10
- package/dist/sql/index.cjs.map +1 -1
- package/dist/sql/index.d.cts +67 -2
- package/dist/sql/index.d.ts +67 -2
- package/dist/sql/index.js +209 -10
- 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,7 +2168,7 @@ var DatabaseHandle = class {
|
|
|
1844
2168
|
var SQLAction = {
|
|
1845
2169
|
READ: "tinycloud.sql/read",
|
|
1846
2170
|
WRITE: "tinycloud.sql/write",
|
|
1847
|
-
|
|
2171
|
+
SCHEMA: "tinycloud.sql/schema",
|
|
1848
2172
|
ADMIN: "tinycloud.sql/admin",
|
|
1849
2173
|
SELECT: "tinycloud.sql/select",
|
|
1850
2174
|
INSERT: "tinycloud.sql/insert",
|
|
@@ -1857,9 +2181,20 @@ var SQLAction = {
|
|
|
1857
2181
|
|
|
1858
2182
|
// src/sql/SQLService.ts
|
|
1859
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";
|
|
1860
2194
|
var SQLService = class extends BaseService {
|
|
1861
2195
|
constructor(config = {}) {
|
|
1862
2196
|
super();
|
|
2197
|
+
this.migrationLocks = /* @__PURE__ */ new Map();
|
|
1863
2198
|
this._config = config;
|
|
1864
2199
|
}
|
|
1865
2200
|
get config() {
|
|
@@ -1895,6 +2230,24 @@ var SQLService = class extends BaseService {
|
|
|
1895
2230
|
async batch(statements, options) {
|
|
1896
2231
|
return this.batchOnDb(this.defaultDbName, statements, options);
|
|
1897
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
|
+
}
|
|
1898
2251
|
// === Internal methods called by DatabaseHandle ===
|
|
1899
2252
|
async queryOnDb(dbName, sql, params, options) {
|
|
1900
2253
|
return this.withTelemetry("query", dbName, async () => {
|
|
@@ -1935,7 +2288,7 @@ var SQLService = class extends BaseService {
|
|
|
1935
2288
|
const actions = [
|
|
1936
2289
|
this.actionForSql(sql, SQLAction.WRITE),
|
|
1937
2290
|
...(options?.schema ?? []).map(
|
|
1938
|
-
(statement) => this.actionForSql(statement, SQLAction.
|
|
2291
|
+
(statement) => this.actionForSql(statement, SQLAction.SCHEMA)
|
|
1939
2292
|
)
|
|
1940
2293
|
];
|
|
1941
2294
|
const response = await this.invokeSQL(
|
|
@@ -2025,6 +2378,78 @@ var SQLService = class extends BaseService {
|
|
|
2025
2378
|
}
|
|
2026
2379
|
});
|
|
2027
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
|
+
}
|
|
2028
2453
|
// === Private helpers ===
|
|
2029
2454
|
async invokeSQL(dbName, actions, body, signal) {
|
|
2030
2455
|
const session = this.context.session;
|
|
@@ -2043,7 +2468,7 @@ var SQLService = class extends BaseService {
|
|
|
2043
2468
|
actionForSql(sql, fallback) {
|
|
2044
2469
|
const token = firstSqlToken(sql);
|
|
2045
2470
|
if (token === "pragma") return SQLAction.ADMIN;
|
|
2046
|
-
if (token !== void 0 && DDL_TOKENS.has(token)) return SQLAction.
|
|
2471
|
+
if (token !== void 0 && DDL_TOKENS.has(token)) return SQLAction.SCHEMA;
|
|
2047
2472
|
return fallback;
|
|
2048
2473
|
}
|
|
2049
2474
|
actionsForSqlBatch(statements) {
|
|
@@ -2072,17 +2497,19 @@ var SQLService = class extends BaseService {
|
|
|
2072
2497
|
}
|
|
2073
2498
|
async handleErrorResponse(response, operation) {
|
|
2074
2499
|
const errorText = await response.text();
|
|
2075
|
-
|
|
2076
|
-
try {
|
|
2077
|
-
errorBody = JSON.parse(errorText);
|
|
2078
|
-
} catch {
|
|
2079
|
-
}
|
|
2500
|
+
const errorBody = parseServiceErrorBody(errorText);
|
|
2080
2501
|
const errorCode = this.mapHttpStatusToErrorCode(
|
|
2081
2502
|
response.status,
|
|
2082
2503
|
errorBody.error
|
|
2083
2504
|
);
|
|
2084
|
-
const message =
|
|
2085
|
-
|
|
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);
|
|
2086
2513
|
if (response.status === 401) {
|
|
2087
2514
|
const { resource, action } = parseAuthError(errorText);
|
|
2088
2515
|
if (action) meta.requiredAction = action;
|
|
@@ -2142,6 +2569,53 @@ function firstSqlToken(sql) {
|
|
|
2142
2569
|
const match = /^[A-Za-z_]+/.exec(sql.slice(index));
|
|
2143
2570
|
return match?.[0].toLowerCase();
|
|
2144
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
|
+
}
|
|
2145
2619
|
|
|
2146
2620
|
// src/duckdb/DuckDbDatabaseHandle.ts
|
|
2147
2621
|
var DuckDbDatabaseHandle = class {
|
|
@@ -2451,17 +2925,19 @@ var DuckDbService = class extends BaseService {
|
|
|
2451
2925
|
}
|
|
2452
2926
|
async handleErrorResponse(response, operation) {
|
|
2453
2927
|
const errorText = await response.text();
|
|
2454
|
-
|
|
2455
|
-
try {
|
|
2456
|
-
errorBody = JSON.parse(errorText);
|
|
2457
|
-
} catch {
|
|
2458
|
-
}
|
|
2928
|
+
const errorBody = parseServiceErrorBody(errorText);
|
|
2459
2929
|
const errorCode = this.mapHttpStatusToErrorCode(
|
|
2460
2930
|
response.status,
|
|
2461
2931
|
errorBody.error
|
|
2462
2932
|
);
|
|
2463
|
-
const message =
|
|
2464
|
-
|
|
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);
|
|
2465
2941
|
if (response.status === 401) {
|
|
2466
2942
|
const { resource, action } = parseAuthError(errorText);
|
|
2467
2943
|
if (action) meta.requiredAction = action;
|
|
@@ -5810,6 +6286,7 @@ EncryptionService.serviceName = "encryption";
|
|
|
5810
6286
|
RetryPolicySchema,
|
|
5811
6287
|
SECRET_NAME_RE,
|
|
5812
6288
|
SQLAction,
|
|
6289
|
+
SQLMigrations,
|
|
5813
6290
|
SQLService,
|
|
5814
6291
|
SecretsService,
|
|
5815
6292
|
ServiceContext,
|
|
@@ -5821,6 +6298,7 @@ EncryptionService.serviceName = "encryption";
|
|
|
5821
6298
|
ServiceSessionSchema,
|
|
5822
6299
|
TelemetryEvents,
|
|
5823
6300
|
TelemetrySpanEventSchema,
|
|
6301
|
+
TinyCloudDebugLogger,
|
|
5824
6302
|
TinyCloudQuota,
|
|
5825
6303
|
VaultHeaders,
|
|
5826
6304
|
VaultPublicSpaceKVActions,
|
|
@@ -5840,21 +6318,26 @@ EncryptionService.serviceName = "encryption";
|
|
|
5840
6318
|
canonicalizeEncryptionJson,
|
|
5841
6319
|
canonicalizeSecretScope,
|
|
5842
6320
|
checkDecryptInvocationInput,
|
|
6321
|
+
clearTinyCloudDebugLogs,
|
|
5843
6322
|
createKVResponseSchema,
|
|
5844
6323
|
createResultSchema,
|
|
5845
6324
|
createVaultCrypto,
|
|
5846
6325
|
decryptEnvelopeWithKey,
|
|
5847
6326
|
defaultRetryPolicy,
|
|
5848
6327
|
deriveSignedReceiverKey,
|
|
6328
|
+
disableTinyCloudDebug,
|
|
5849
6329
|
discoverNetwork,
|
|
6330
|
+
enableTinyCloudDebug,
|
|
5850
6331
|
encryptToNetwork,
|
|
5851
6332
|
encryptionError,
|
|
5852
6333
|
ensureNetworkUsableForDecrypt,
|
|
5853
6334
|
err,
|
|
5854
6335
|
errorResult,
|
|
5855
6336
|
generateRandomReceiverKey,
|
|
6337
|
+
getTinyCloudDebugLogs,
|
|
5856
6338
|
hexDecode,
|
|
5857
6339
|
hexEncode,
|
|
6340
|
+
installTinyCloudDebugGlobals,
|
|
5858
6341
|
isNetworkId,
|
|
5859
6342
|
networkDiscoveryKey,
|
|
5860
6343
|
networkError,
|
|
@@ -5870,6 +6353,7 @@ EncryptionService.serviceName = "encryption";
|
|
|
5870
6353
|
storageLimitReachedError,
|
|
5871
6354
|
storageQuotaExceededError,
|
|
5872
6355
|
timeoutError,
|
|
6356
|
+
tinyCloudDebugLogger,
|
|
5873
6357
|
utf8Decode,
|
|
5874
6358
|
utf8Encode,
|
|
5875
6359
|
validateEnvelope,
|