@tinycloud/cli 0.7.0-beta.10 → 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 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
- let errorBody = {};
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 = errorBody.message || `SQL ${operation} failed: ${response.status} - ${errorText}`;
6711
- const meta = { status: response.status, statusText: response.statusText };
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
- let errorBody = {};
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 = errorBody.message || `DuckDB ${operation} failed: ${response.status} - ${errorText}`;
7084
- const meta = { status: response.status, statusText: response.statusText };
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;
@@ -15550,7 +15746,7 @@ function registerAccountCommand(program2) {
15550
15746
  try {
15551
15747
  const options = _options;
15552
15748
  const node = await authenticatedNode(cmd);
15553
- const result = options.live ? await node.account.applications.list() : await node.account.index.applications.list();
15749
+ const result = options.live ? await node.account.applications.list() : await node.account.applications.list({ preferIndex: true });
15554
15750
  assertOk(result);
15555
15751
  const payload = { applications: result.data, count: result.data.length };
15556
15752
  if (shouldOutputJson()) {
@@ -15580,7 +15776,7 @@ function registerAccountCommand(program2) {
15580
15776
  spaces.command("list").description("List spaces registered under account/spaces").option("--live", "Read canonical account KV records instead of the SQLite index").action(async (options, cmd) => {
15581
15777
  try {
15582
15778
  const node = await authenticatedNode(cmd);
15583
- const result = options.live ? await node.account.spaces.list() : await node.account.index.spaces.list();
15779
+ const result = options.live ? await node.account.spaces.list() : await node.account.spaces.list({ preferIndex: true });
15584
15780
  assertOk(result);
15585
15781
  const payload = { spaces: result.data.map(formatSpace), count: result.data.length };
15586
15782
  if (shouldOutputJson()) {
@@ -15693,7 +15889,7 @@ function registerAccountCommand(program2) {
15693
15889
  }
15694
15890
  const node = await authenticatedNode(cmd);
15695
15891
  const direction = options.granted ? "granted" : options.received ? "received" : "all";
15696
- const result = options.live ? await node.account.delegations.list({ direction, space: options.space }) : await node.account.index.delegations.list({ direction, space: options.space });
15892
+ const result = options.live ? await node.account.delegations.list({ direction, space: options.space }) : await node.account.delegations.list({ direction, space: options.space, preferIndex: true });
15697
15893
  assertOk(result);
15698
15894
  const payload = { delegations: result.data.map(formatDelegation2), count: result.data.length };
15699
15895
  if (shouldOutputJson()) {