@tinycloud/cli 0.7.0-beta.11 → 0.7.0-beta.12
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 +210 -14
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -6448,10 +6448,54 @@ var KVService = class extends BaseService {
|
|
|
6448
6448
|
}
|
|
6449
6449
|
};
|
|
6450
6450
|
KVService.serviceName = "kv";
|
|
6451
|
+
function parseServiceErrorBody(errorText) {
|
|
6452
|
+
try {
|
|
6453
|
+
return JSON.parse(errorText);
|
|
6454
|
+
} catch {
|
|
6455
|
+
return {};
|
|
6456
|
+
}
|
|
6457
|
+
}
|
|
6458
|
+
function formatServiceResponseError(serviceLabel, operation, status, errorText, parsed) {
|
|
6459
|
+
if (parsed.message) {
|
|
6460
|
+
return compactMessage(parsed.message);
|
|
6461
|
+
}
|
|
6462
|
+
if (looksLikeHtml(errorText)) {
|
|
6463
|
+
if (status === 524 || /524\s*[:-]/i.test(errorText) || /a timeout occurred/i.test(errorText)) {
|
|
6464
|
+
return `${serviceLabel} ${operation} failed: upstream request timed out. Please retry.`;
|
|
6465
|
+
}
|
|
6466
|
+
return `${serviceLabel} ${operation} failed: upstream service returned an HTML error page (${status}).`;
|
|
6467
|
+
}
|
|
6468
|
+
return `${serviceLabel} ${operation} failed: ${status} - ${compactMessage(errorText)}`;
|
|
6469
|
+
}
|
|
6470
|
+
function responseErrorMeta(status, statusText, errorText) {
|
|
6471
|
+
const meta = { status, statusText };
|
|
6472
|
+
const snippet = compactMessage(errorText);
|
|
6473
|
+
if (snippet) {
|
|
6474
|
+
meta.responseSnippet = snippet.slice(0, 300);
|
|
6475
|
+
}
|
|
6476
|
+
return meta;
|
|
6477
|
+
}
|
|
6478
|
+
function looksLikeHtml(text) {
|
|
6479
|
+
return /<!doctype html/i.test(text) || /<html[\s>]/i.test(text);
|
|
6480
|
+
}
|
|
6481
|
+
function compactMessage(message) {
|
|
6482
|
+
const compact = message.replace(/\s+/g, " ").trim();
|
|
6483
|
+
return compact.length > 500 ? `${compact.slice(0, 500)}...` : compact;
|
|
6484
|
+
}
|
|
6485
|
+
var SQLMigrations = class {
|
|
6486
|
+
constructor(service, dbName) {
|
|
6487
|
+
this.service = service;
|
|
6488
|
+
this.dbName = dbName;
|
|
6489
|
+
}
|
|
6490
|
+
apply(options) {
|
|
6491
|
+
return this.service.applyMigrationsOnDb(this.dbName, options);
|
|
6492
|
+
}
|
|
6493
|
+
};
|
|
6451
6494
|
var DatabaseHandle = class {
|
|
6452
6495
|
constructor(service, name) {
|
|
6453
6496
|
this.service = service;
|
|
6454
6497
|
this.name = name;
|
|
6498
|
+
this.migrations = new SQLMigrations(service, name);
|
|
6455
6499
|
}
|
|
6456
6500
|
async query(sql, params, options) {
|
|
6457
6501
|
return this.service.queryOnDb(this.name, sql, params, options);
|
|
@@ -6483,9 +6527,20 @@ var SQLAction = {
|
|
|
6483
6527
|
ALL: "tinycloud.sql/*"
|
|
6484
6528
|
};
|
|
6485
6529
|
var DDL_TOKENS = /* @__PURE__ */ new Set(["alter", "create", "drop"]);
|
|
6530
|
+
var MIGRATIONS_TABLE = "__tinycloud_sql_migrations";
|
|
6531
|
+
var MIGRATIONS_SCHEMA = `CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (
|
|
6532
|
+
key TEXT PRIMARY KEY,
|
|
6533
|
+
namespace TEXT NOT NULL,
|
|
6534
|
+
id TEXT NOT NULL,
|
|
6535
|
+
applied_at TEXT NOT NULL,
|
|
6536
|
+
statement_count INTEGER NOT NULL
|
|
6537
|
+
)`;
|
|
6538
|
+
var MIGRATIONS_META_NAMESPACE = "tinycloud.sql.migrations";
|
|
6539
|
+
var MIGRATIONS_META_ID = "000_metadata";
|
|
6486
6540
|
var SQLService = class extends BaseService {
|
|
6487
6541
|
constructor(config = {}) {
|
|
6488
6542
|
super();
|
|
6543
|
+
this.migrationLocks = /* @__PURE__ */ new Map();
|
|
6489
6544
|
this._config = config;
|
|
6490
6545
|
}
|
|
6491
6546
|
get config() {
|
|
@@ -6521,6 +6576,24 @@ var SQLService = class extends BaseService {
|
|
|
6521
6576
|
async batch(statements, options) {
|
|
6522
6577
|
return this.batchOnDb(this.defaultDbName, statements, options);
|
|
6523
6578
|
}
|
|
6579
|
+
async applyMigrationsOnDb(dbName, options) {
|
|
6580
|
+
const validationError = validateMigrationOptions(options);
|
|
6581
|
+
if (validationError) {
|
|
6582
|
+
return err(serviceError(ErrorCodes.INVALID_INPUT, validationError, "sql"));
|
|
6583
|
+
}
|
|
6584
|
+
const lockKey = `${dbName}\0${options.namespace}`;
|
|
6585
|
+
const existing = this.migrationLocks.get(lockKey);
|
|
6586
|
+
if (existing) {
|
|
6587
|
+
return existing;
|
|
6588
|
+
}
|
|
6589
|
+
const promise = this.applyMigrationsOnDbUnlocked(dbName, options);
|
|
6590
|
+
this.migrationLocks.set(lockKey, promise);
|
|
6591
|
+
try {
|
|
6592
|
+
return await promise;
|
|
6593
|
+
} finally {
|
|
6594
|
+
this.migrationLocks.delete(lockKey);
|
|
6595
|
+
}
|
|
6596
|
+
}
|
|
6524
6597
|
// === Internal methods called by DatabaseHandle ===
|
|
6525
6598
|
async queryOnDb(dbName, sql, params, options) {
|
|
6526
6599
|
return this.withTelemetry("query", dbName, async () => {
|
|
@@ -6651,6 +6724,78 @@ var SQLService = class extends BaseService {
|
|
|
6651
6724
|
}
|
|
6652
6725
|
});
|
|
6653
6726
|
}
|
|
6727
|
+
async applyMigrationsOnDbUnlocked(dbName, options) {
|
|
6728
|
+
return this.withTelemetry("migrations.apply", dbName, async () => {
|
|
6729
|
+
const created = await this.ensureMigrationsTable(dbName, options.signal);
|
|
6730
|
+
if (!created.ok) return created;
|
|
6731
|
+
const listed = await this.queryOnDb(
|
|
6732
|
+
dbName,
|
|
6733
|
+
`SELECT id FROM ${MIGRATIONS_TABLE} WHERE namespace = ? ORDER BY applied_at, id`,
|
|
6734
|
+
[options.namespace],
|
|
6735
|
+
{ signal: options.signal }
|
|
6736
|
+
);
|
|
6737
|
+
if (!listed.ok) return listed;
|
|
6738
|
+
const appliedIds = new Set(
|
|
6739
|
+
listed.data.rows.map((row) => rowValue(row, 0)).filter((id) => typeof id === "string")
|
|
6740
|
+
);
|
|
6741
|
+
const skipped = options.migrations.filter((migration) => appliedIds.has(migration.id)).map((migration) => migration.id);
|
|
6742
|
+
const pending = options.migrations.filter((migration) => !appliedIds.has(migration.id));
|
|
6743
|
+
const applied = [];
|
|
6744
|
+
for (const migration of pending) {
|
|
6745
|
+
const result = await this.applyOneMigration(dbName, options.namespace, migration, options.signal);
|
|
6746
|
+
if (!result.ok) return result;
|
|
6747
|
+
applied.push(migration.id);
|
|
6748
|
+
}
|
|
6749
|
+
return ok({
|
|
6750
|
+
database: dbName,
|
|
6751
|
+
namespace: options.namespace,
|
|
6752
|
+
status: applied.length > 0 ? "applied" : "already_current",
|
|
6753
|
+
applied,
|
|
6754
|
+
skipped
|
|
6755
|
+
});
|
|
6756
|
+
});
|
|
6757
|
+
}
|
|
6758
|
+
async applyOneMigration(dbName, namespace, migration, signal) {
|
|
6759
|
+
const statements = [
|
|
6760
|
+
...migration.sql.map(
|
|
6761
|
+
(statement) => typeof statement === "string" ? { sql: statement } : statement
|
|
6762
|
+
),
|
|
6763
|
+
{
|
|
6764
|
+
sql: `INSERT OR REPLACE INTO ${MIGRATIONS_TABLE} (key, namespace, id, applied_at, statement_count) VALUES (?, ?, ?, ?, ?)`,
|
|
6765
|
+
params: [
|
|
6766
|
+
migrationKey(namespace, migration.id),
|
|
6767
|
+
namespace,
|
|
6768
|
+
migration.id,
|
|
6769
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
6770
|
+
migration.sql.length
|
|
6771
|
+
]
|
|
6772
|
+
}
|
|
6773
|
+
];
|
|
6774
|
+
const result = await this.batchOnDb(dbName, statements, { signal });
|
|
6775
|
+
if (!result.ok) return result;
|
|
6776
|
+
return ok(void 0);
|
|
6777
|
+
}
|
|
6778
|
+
async ensureMigrationsTable(dbName, signal) {
|
|
6779
|
+
const result = await this.batchOnDb(
|
|
6780
|
+
dbName,
|
|
6781
|
+
[
|
|
6782
|
+
{ sql: MIGRATIONS_SCHEMA },
|
|
6783
|
+
{
|
|
6784
|
+
sql: `INSERT OR REPLACE INTO ${MIGRATIONS_TABLE} (key, namespace, id, applied_at, statement_count) VALUES (?, ?, ?, ?, ?)`,
|
|
6785
|
+
params: [
|
|
6786
|
+
migrationKey(MIGRATIONS_META_NAMESPACE, MIGRATIONS_META_ID),
|
|
6787
|
+
MIGRATIONS_META_NAMESPACE,
|
|
6788
|
+
MIGRATIONS_META_ID,
|
|
6789
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
6790
|
+
1
|
|
6791
|
+
]
|
|
6792
|
+
}
|
|
6793
|
+
],
|
|
6794
|
+
{ signal }
|
|
6795
|
+
);
|
|
6796
|
+
if (!result.ok) return result;
|
|
6797
|
+
return ok(void 0);
|
|
6798
|
+
}
|
|
6654
6799
|
// === Private helpers ===
|
|
6655
6800
|
async invokeSQL(dbName, actions, body, signal) {
|
|
6656
6801
|
const session = this.context.session;
|
|
@@ -6698,17 +6843,19 @@ var SQLService = class extends BaseService {
|
|
|
6698
6843
|
}
|
|
6699
6844
|
async handleErrorResponse(response, operation) {
|
|
6700
6845
|
const errorText = await response.text();
|
|
6701
|
-
|
|
6702
|
-
try {
|
|
6703
|
-
errorBody = JSON.parse(errorText);
|
|
6704
|
-
} catch {
|
|
6705
|
-
}
|
|
6846
|
+
const errorBody = parseServiceErrorBody(errorText);
|
|
6706
6847
|
const errorCode = this.mapHttpStatusToErrorCode(
|
|
6707
6848
|
response.status,
|
|
6708
6849
|
errorBody.error
|
|
6709
6850
|
);
|
|
6710
|
-
const message =
|
|
6711
|
-
|
|
6851
|
+
const message = formatServiceResponseError(
|
|
6852
|
+
"SQL",
|
|
6853
|
+
operation,
|
|
6854
|
+
response.status,
|
|
6855
|
+
errorText,
|
|
6856
|
+
errorBody
|
|
6857
|
+
);
|
|
6858
|
+
const meta = responseErrorMeta(response.status, response.statusText, errorText);
|
|
6712
6859
|
if (response.status === 401) {
|
|
6713
6860
|
const { resource, action } = parseAuthError(errorText);
|
|
6714
6861
|
if (action) meta.requiredAction = action;
|
|
@@ -6768,6 +6915,53 @@ function firstSqlToken(sql) {
|
|
|
6768
6915
|
const match = /^[A-Za-z_]+/.exec(sql.slice(index));
|
|
6769
6916
|
return match?.[0].toLowerCase();
|
|
6770
6917
|
}
|
|
6918
|
+
function validateMigrationOptions(options) {
|
|
6919
|
+
if (!options || typeof options !== "object") {
|
|
6920
|
+
return "SQL migrations require options";
|
|
6921
|
+
}
|
|
6922
|
+
if (!options.namespace || typeof options.namespace !== "string") {
|
|
6923
|
+
return "SQL migrations require a non-empty namespace";
|
|
6924
|
+
}
|
|
6925
|
+
if (!Array.isArray(options.migrations)) {
|
|
6926
|
+
return "SQL migrations require an ordered migrations array";
|
|
6927
|
+
}
|
|
6928
|
+
const ids = /* @__PURE__ */ new Set();
|
|
6929
|
+
for (const migration of options.migrations) {
|
|
6930
|
+
if (!migration.id || typeof migration.id !== "string") {
|
|
6931
|
+
return "SQL migrations require every migration to have a non-empty id";
|
|
6932
|
+
}
|
|
6933
|
+
if (ids.has(migration.id)) {
|
|
6934
|
+
return `Duplicate SQL migration id: ${migration.id}`;
|
|
6935
|
+
}
|
|
6936
|
+
ids.add(migration.id);
|
|
6937
|
+
if (!Array.isArray(migration.sql)) {
|
|
6938
|
+
return `SQL migration ${migration.id} requires a sql array`;
|
|
6939
|
+
}
|
|
6940
|
+
for (const statement of migration.sql) {
|
|
6941
|
+
if (typeof statement === "string") {
|
|
6942
|
+
if (statement.trim() === "") {
|
|
6943
|
+
return `SQL migration ${migration.id} contains an empty statement`;
|
|
6944
|
+
}
|
|
6945
|
+
continue;
|
|
6946
|
+
}
|
|
6947
|
+
if (!statement || typeof statement.sql !== "string" || statement.sql.trim() === "") {
|
|
6948
|
+
return `SQL migration ${migration.id} contains an invalid statement`;
|
|
6949
|
+
}
|
|
6950
|
+
}
|
|
6951
|
+
}
|
|
6952
|
+
return null;
|
|
6953
|
+
}
|
|
6954
|
+
function migrationKey(namespace, id) {
|
|
6955
|
+
return `${encodeURIComponent(namespace)}:${encodeURIComponent(id)}`;
|
|
6956
|
+
}
|
|
6957
|
+
function rowValue(row, index) {
|
|
6958
|
+
if (Array.isArray(row)) return row[index];
|
|
6959
|
+
if (row && typeof row === "object") {
|
|
6960
|
+
const values = Object.values(row);
|
|
6961
|
+
return values[index];
|
|
6962
|
+
}
|
|
6963
|
+
return void 0;
|
|
6964
|
+
}
|
|
6771
6965
|
var DuckDbDatabaseHandle = class {
|
|
6772
6966
|
constructor(service, name) {
|
|
6773
6967
|
this.service = service;
|
|
@@ -7071,17 +7265,19 @@ var DuckDbService = class extends BaseService {
|
|
|
7071
7265
|
}
|
|
7072
7266
|
async handleErrorResponse(response, operation) {
|
|
7073
7267
|
const errorText = await response.text();
|
|
7074
|
-
|
|
7075
|
-
try {
|
|
7076
|
-
errorBody = JSON.parse(errorText);
|
|
7077
|
-
} catch {
|
|
7078
|
-
}
|
|
7268
|
+
const errorBody = parseServiceErrorBody(errorText);
|
|
7079
7269
|
const errorCode = this.mapHttpStatusToErrorCode(
|
|
7080
7270
|
response.status,
|
|
7081
7271
|
errorBody.error
|
|
7082
7272
|
);
|
|
7083
|
-
const message =
|
|
7084
|
-
|
|
7273
|
+
const message = formatServiceResponseError(
|
|
7274
|
+
"DuckDB",
|
|
7275
|
+
operation,
|
|
7276
|
+
response.status,
|
|
7277
|
+
errorText,
|
|
7278
|
+
errorBody
|
|
7279
|
+
);
|
|
7280
|
+
const meta = responseErrorMeta(response.status, response.statusText, errorText);
|
|
7085
7281
|
if (response.status === 401) {
|
|
7086
7282
|
const { resource, action } = parseAuthError(errorText);
|
|
7087
7283
|
if (action) meta.requiredAction = action;
|