@tinycloud/sdk-services 2.4.0-beta.1 → 2.4.0-beta.11

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.
@@ -85,12 +85,54 @@ interface ExecuteResponse {
85
85
  interface BatchResponse {
86
86
  results: ExecuteResponse[];
87
87
  }
88
+ /**
89
+ * A versioned SQL migration owned by an application or SDK subsystem.
90
+ */
91
+ interface SqlMigration {
92
+ /**
93
+ * Stable migration id within the namespace, e.g. "001_initial".
94
+ */
95
+ id: string;
96
+ /**
97
+ * SQL statements to apply when this migration has not been recorded.
98
+ * Statements should be idempotent where SQLite supports it.
99
+ */
100
+ sql: Array<string | SqlStatement>;
101
+ }
102
+ /**
103
+ * Options for applying SQL migrations.
104
+ */
105
+ interface SqlMigrationApplyOptions {
106
+ /**
107
+ * Namespace for this migration set, usually an app id or SDK subsystem.
108
+ */
109
+ namespace: string;
110
+ /**
111
+ * Ordered migration list.
112
+ */
113
+ migrations: SqlMigration[];
114
+ /**
115
+ * Custom abort signal for migration operations.
116
+ */
117
+ signal?: AbortSignal;
118
+ }
119
+ /**
120
+ * Result from applying SQL migrations.
121
+ */
122
+ interface SqlMigrationApplyResponse {
123
+ database: string;
124
+ namespace: string;
125
+ status: "already_current" | "applied";
126
+ applied: string[];
127
+ skipped: string[];
128
+ }
88
129
  /**
89
130
  * SQL service action types.
90
131
  */
91
132
  declare const SQLAction: {
92
133
  readonly READ: "tinycloud.sql/read";
93
134
  readonly WRITE: "tinycloud.sql/write";
135
+ readonly DDL: "tinycloud.sql/ddl";
94
136
  readonly ADMIN: "tinycloud.sql/admin";
95
137
  readonly SELECT: "tinycloud.sql/select";
96
138
  readonly INSERT: "tinycloud.sql/insert";
@@ -108,12 +150,23 @@ type SQLActionType = (typeof SQLAction)[keyof typeof SQLAction];
108
150
  * Platform-agnostic interface for SQL database operations.
109
151
  */
110
152
 
153
+ /**
154
+ * Migration helper bound to a specific database.
155
+ */
156
+ interface ISQLMigrations {
157
+ /**
158
+ * Apply ordered, app-owned migrations and record completed ids.
159
+ */
160
+ apply(options: SqlMigrationApplyOptions): Promise<Result<SqlMigrationApplyResponse>>;
161
+ }
111
162
  /**
112
163
  * Database handle interface for operations on a specific named database.
113
164
  */
114
165
  interface IDatabaseHandle {
115
166
  /** The database name */
116
167
  readonly name: string;
168
+ /** Versioned schema migration helper. */
169
+ readonly migrations: ISQLMigrations;
117
170
  /**
118
171
  * Execute a SQL query and return rows.
119
172
  */
@@ -178,6 +231,7 @@ interface ISQLService extends IService {
178
231
  declare class SQLService extends BaseService implements ISQLService {
179
232
  static readonly serviceName = "sql";
180
233
  protected _config: SQLServiceConfig;
234
+ private readonly migrationLocks;
181
235
  constructor(config?: SQLServiceConfig);
182
236
  get config(): SQLServiceConfig;
183
237
  private get defaultDbName();
@@ -198,14 +252,20 @@ declare class SQLService extends BaseService implements ISQLService {
198
252
  * Shortcut: batch on the default database.
199
253
  */
200
254
  batch(statements: SqlStatement[], options?: BatchOptions): Promise<Result<BatchResponse>>;
255
+ applyMigrationsOnDb(dbName: string, options: SqlMigrationApplyOptions): Promise<Result<SqlMigrationApplyResponse>>;
201
256
  queryOnDb<T = Record<string, unknown>>(dbName: string, sql: string, params?: SqlValue[], options?: QueryOptions): Promise<Result<QueryResponse<T>>>;
202
257
  executeOnDb(dbName: string, sql: string, params?: SqlValue[], options?: ExecuteOptions): Promise<Result<ExecuteResponse>>;
203
258
  batchOnDb(dbName: string, statements: SqlStatement[], options?: BatchOptions): Promise<Result<BatchResponse>>;
204
259
  executeStatementOnDb(dbName: string, name: string, params?: SqlValue[], options?: QueryOptions): Promise<Result<QueryResponse | ExecuteResponse>>;
205
260
  exportDb(dbName: string, options?: QueryOptions): Promise<Result<Blob>>;
261
+ private applyMigrationsOnDbUnlocked;
262
+ private applyOneMigration;
263
+ private ensureMigrationsTable;
206
264
  private invokeSQL;
207
265
  private actionForSql;
208
- private actionForSqlBatch;
266
+ private actionsForSqlBatch;
267
+ private dedupeActions;
268
+ private invokeSQLAny;
209
269
  private handleErrorResponse;
210
270
  private mapHttpStatusToErrorCode;
211
271
  }
@@ -219,6 +279,7 @@ declare class SQLService extends BaseService implements ISQLService {
219
279
  declare class DatabaseHandle implements IDatabaseHandle {
220
280
  private service;
221
281
  readonly name: string;
282
+ readonly migrations: ISQLMigrations;
222
283
  constructor(service: SQLService, name: string);
223
284
  query<T = Record<string, unknown>>(sql: string, params?: SqlValue[], options?: QueryOptions): Promise<Result<QueryResponse<T>>>;
224
285
  execute(sql: string, params?: SqlValue[], options?: ExecuteOptions): Promise<Result<ExecuteResponse>>;
@@ -227,4 +288,11 @@ declare class DatabaseHandle implements IDatabaseHandle {
227
288
  export(options?: QueryOptions): Promise<Result<Blob>>;
228
289
  }
229
290
 
230
- export { type BatchOptions, type BatchResponse, DatabaseHandle, type ExecuteOptions, type ExecuteResponse, type IDatabaseHandle, type ISQLService, type QueryOptions, type QueryResponse, SQLAction, type SQLActionType, SQLService, type SQLServiceConfig, type SqlStatement, type SqlValue };
291
+ declare class SQLMigrations implements ISQLMigrations {
292
+ private readonly service;
293
+ private readonly dbName;
294
+ constructor(service: SQLService, dbName: string);
295
+ apply(options: SqlMigrationApplyOptions): Promise<Result<SqlMigrationApplyResponse>>;
296
+ }
297
+
298
+ export { type BatchOptions, type BatchResponse, DatabaseHandle, type ExecuteOptions, type ExecuteResponse, type IDatabaseHandle, type ISQLMigrations, type ISQLService, type QueryOptions, type QueryResponse, SQLAction, type SQLActionType, SQLMigrations, SQLService, type SQLServiceConfig, type SqlMigration, type SqlMigrationApplyOptions, type SqlMigrationApplyResponse, type SqlStatement, type SqlValue };
@@ -85,12 +85,54 @@ interface ExecuteResponse {
85
85
  interface BatchResponse {
86
86
  results: ExecuteResponse[];
87
87
  }
88
+ /**
89
+ * A versioned SQL migration owned by an application or SDK subsystem.
90
+ */
91
+ interface SqlMigration {
92
+ /**
93
+ * Stable migration id within the namespace, e.g. "001_initial".
94
+ */
95
+ id: string;
96
+ /**
97
+ * SQL statements to apply when this migration has not been recorded.
98
+ * Statements should be idempotent where SQLite supports it.
99
+ */
100
+ sql: Array<string | SqlStatement>;
101
+ }
102
+ /**
103
+ * Options for applying SQL migrations.
104
+ */
105
+ interface SqlMigrationApplyOptions {
106
+ /**
107
+ * Namespace for this migration set, usually an app id or SDK subsystem.
108
+ */
109
+ namespace: string;
110
+ /**
111
+ * Ordered migration list.
112
+ */
113
+ migrations: SqlMigration[];
114
+ /**
115
+ * Custom abort signal for migration operations.
116
+ */
117
+ signal?: AbortSignal;
118
+ }
119
+ /**
120
+ * Result from applying SQL migrations.
121
+ */
122
+ interface SqlMigrationApplyResponse {
123
+ database: string;
124
+ namespace: string;
125
+ status: "already_current" | "applied";
126
+ applied: string[];
127
+ skipped: string[];
128
+ }
88
129
  /**
89
130
  * SQL service action types.
90
131
  */
91
132
  declare const SQLAction: {
92
133
  readonly READ: "tinycloud.sql/read";
93
134
  readonly WRITE: "tinycloud.sql/write";
135
+ readonly DDL: "tinycloud.sql/ddl";
94
136
  readonly ADMIN: "tinycloud.sql/admin";
95
137
  readonly SELECT: "tinycloud.sql/select";
96
138
  readonly INSERT: "tinycloud.sql/insert";
@@ -108,12 +150,23 @@ type SQLActionType = (typeof SQLAction)[keyof typeof SQLAction];
108
150
  * Platform-agnostic interface for SQL database operations.
109
151
  */
110
152
 
153
+ /**
154
+ * Migration helper bound to a specific database.
155
+ */
156
+ interface ISQLMigrations {
157
+ /**
158
+ * Apply ordered, app-owned migrations and record completed ids.
159
+ */
160
+ apply(options: SqlMigrationApplyOptions): Promise<Result<SqlMigrationApplyResponse>>;
161
+ }
111
162
  /**
112
163
  * Database handle interface for operations on a specific named database.
113
164
  */
114
165
  interface IDatabaseHandle {
115
166
  /** The database name */
116
167
  readonly name: string;
168
+ /** Versioned schema migration helper. */
169
+ readonly migrations: ISQLMigrations;
117
170
  /**
118
171
  * Execute a SQL query and return rows.
119
172
  */
@@ -178,6 +231,7 @@ interface ISQLService extends IService {
178
231
  declare class SQLService extends BaseService implements ISQLService {
179
232
  static readonly serviceName = "sql";
180
233
  protected _config: SQLServiceConfig;
234
+ private readonly migrationLocks;
181
235
  constructor(config?: SQLServiceConfig);
182
236
  get config(): SQLServiceConfig;
183
237
  private get defaultDbName();
@@ -198,14 +252,20 @@ declare class SQLService extends BaseService implements ISQLService {
198
252
  * Shortcut: batch on the default database.
199
253
  */
200
254
  batch(statements: SqlStatement[], options?: BatchOptions): Promise<Result<BatchResponse>>;
255
+ applyMigrationsOnDb(dbName: string, options: SqlMigrationApplyOptions): Promise<Result<SqlMigrationApplyResponse>>;
201
256
  queryOnDb<T = Record<string, unknown>>(dbName: string, sql: string, params?: SqlValue[], options?: QueryOptions): Promise<Result<QueryResponse<T>>>;
202
257
  executeOnDb(dbName: string, sql: string, params?: SqlValue[], options?: ExecuteOptions): Promise<Result<ExecuteResponse>>;
203
258
  batchOnDb(dbName: string, statements: SqlStatement[], options?: BatchOptions): Promise<Result<BatchResponse>>;
204
259
  executeStatementOnDb(dbName: string, name: string, params?: SqlValue[], options?: QueryOptions): Promise<Result<QueryResponse | ExecuteResponse>>;
205
260
  exportDb(dbName: string, options?: QueryOptions): Promise<Result<Blob>>;
261
+ private applyMigrationsOnDbUnlocked;
262
+ private applyOneMigration;
263
+ private ensureMigrationsTable;
206
264
  private invokeSQL;
207
265
  private actionForSql;
208
- private actionForSqlBatch;
266
+ private actionsForSqlBatch;
267
+ private dedupeActions;
268
+ private invokeSQLAny;
209
269
  private handleErrorResponse;
210
270
  private mapHttpStatusToErrorCode;
211
271
  }
@@ -219,6 +279,7 @@ declare class SQLService extends BaseService implements ISQLService {
219
279
  declare class DatabaseHandle implements IDatabaseHandle {
220
280
  private service;
221
281
  readonly name: string;
282
+ readonly migrations: ISQLMigrations;
222
283
  constructor(service: SQLService, name: string);
223
284
  query<T = Record<string, unknown>>(sql: string, params?: SqlValue[], options?: QueryOptions): Promise<Result<QueryResponse<T>>>;
224
285
  execute(sql: string, params?: SqlValue[], options?: ExecuteOptions): Promise<Result<ExecuteResponse>>;
@@ -227,4 +288,11 @@ declare class DatabaseHandle implements IDatabaseHandle {
227
288
  export(options?: QueryOptions): Promise<Result<Blob>>;
228
289
  }
229
290
 
230
- export { type BatchOptions, type BatchResponse, DatabaseHandle, type ExecuteOptions, type ExecuteResponse, type IDatabaseHandle, type ISQLService, type QueryOptions, type QueryResponse, SQLAction, type SQLActionType, SQLService, type SQLServiceConfig, type SqlStatement, type SqlValue };
291
+ declare class SQLMigrations implements ISQLMigrations {
292
+ private readonly service;
293
+ private readonly dbName;
294
+ constructor(service: SQLService, dbName: string);
295
+ apply(options: SqlMigrationApplyOptions): Promise<Result<SqlMigrationApplyResponse>>;
296
+ }
297
+
298
+ export { type BatchOptions, type BatchResponse, DatabaseHandle, type ExecuteOptions, type ExecuteResponse, type IDatabaseHandle, type ISQLMigrations, type ISQLService, type QueryOptions, type QueryResponse, SQLAction, type SQLActionType, SQLMigrations, SQLService, type SQLServiceConfig, type SqlMigration, type SqlMigrationApplyOptions, type SqlMigrationApplyResponse, type SqlStatement, type SqlValue };
package/dist/sql/index.js CHANGED
@@ -324,11 +324,59 @@ var BaseService = class {
324
324
  }
325
325
  };
326
326
 
327
+ // src/responseErrors.ts
328
+ function parseServiceErrorBody(errorText) {
329
+ try {
330
+ return JSON.parse(errorText);
331
+ } catch {
332
+ return {};
333
+ }
334
+ }
335
+ function formatServiceResponseError(serviceLabel, operation, status, errorText, parsed) {
336
+ if (parsed.message) {
337
+ return compactMessage(parsed.message);
338
+ }
339
+ if (looksLikeHtml(errorText)) {
340
+ if (status === 524 || /524\s*[:-]/i.test(errorText) || /a timeout occurred/i.test(errorText)) {
341
+ return `${serviceLabel} ${operation} failed: upstream request timed out. Please retry.`;
342
+ }
343
+ return `${serviceLabel} ${operation} failed: upstream service returned an HTML error page (${status}).`;
344
+ }
345
+ return `${serviceLabel} ${operation} failed: ${status} - ${compactMessage(errorText)}`;
346
+ }
347
+ function responseErrorMeta(status, statusText, errorText) {
348
+ const meta = { status, statusText };
349
+ const snippet = compactMessage(errorText);
350
+ if (snippet) {
351
+ meta.responseSnippet = snippet.slice(0, 300);
352
+ }
353
+ return meta;
354
+ }
355
+ function looksLikeHtml(text) {
356
+ return /<!doctype html/i.test(text) || /<html[\s>]/i.test(text);
357
+ }
358
+ function compactMessage(message) {
359
+ const compact = message.replace(/\s+/g, " ").trim();
360
+ return compact.length > 500 ? `${compact.slice(0, 500)}...` : compact;
361
+ }
362
+
363
+ // src/sql/SQLMigrations.ts
364
+ var SQLMigrations = class {
365
+ constructor(service, dbName) {
366
+ this.service = service;
367
+ this.dbName = dbName;
368
+ }
369
+ apply(options) {
370
+ return this.service.applyMigrationsOnDb(this.dbName, options);
371
+ }
372
+ };
373
+
327
374
  // src/sql/DatabaseHandle.ts
328
375
  var DatabaseHandle = class {
329
376
  constructor(service, name) {
330
377
  this.service = service;
331
378
  this.name = name;
379
+ this.migrations = new SQLMigrations(service, name);
332
380
  }
333
381
  async query(sql, params, options) {
334
382
  return this.service.queryOnDb(this.name, sql, params, options);
@@ -351,6 +399,7 @@ var DatabaseHandle = class {
351
399
  var SQLAction = {
352
400
  READ: "tinycloud.sql/read",
353
401
  WRITE: "tinycloud.sql/write",
402
+ DDL: "tinycloud.sql/ddl",
354
403
  ADMIN: "tinycloud.sql/admin",
355
404
  SELECT: "tinycloud.sql/select",
356
405
  INSERT: "tinycloud.sql/insert",
@@ -362,9 +411,21 @@ var SQLAction = {
362
411
  };
363
412
 
364
413
  // src/sql/SQLService.ts
414
+ var DDL_TOKENS = /* @__PURE__ */ new Set(["alter", "create", "drop"]);
415
+ var MIGRATIONS_TABLE = "__tinycloud_sql_migrations";
416
+ var MIGRATIONS_SCHEMA = `CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (
417
+ key TEXT PRIMARY KEY,
418
+ namespace TEXT NOT NULL,
419
+ id TEXT NOT NULL,
420
+ applied_at TEXT NOT NULL,
421
+ statement_count INTEGER NOT NULL
422
+ )`;
423
+ var MIGRATIONS_META_NAMESPACE = "tinycloud.sql.migrations";
424
+ var MIGRATIONS_META_ID = "000_metadata";
365
425
  var SQLService = class extends BaseService {
366
426
  constructor(config = {}) {
367
427
  super();
428
+ this.migrationLocks = /* @__PURE__ */ new Map();
368
429
  this._config = config;
369
430
  }
370
431
  get config() {
@@ -400,6 +461,24 @@ var SQLService = class extends BaseService {
400
461
  async batch(statements, options) {
401
462
  return this.batchOnDb(this.defaultDbName, statements, options);
402
463
  }
464
+ async applyMigrationsOnDb(dbName, options) {
465
+ const validationError = validateMigrationOptions(options);
466
+ if (validationError) {
467
+ return err(serviceError(ErrorCodes.INVALID_INPUT, validationError, "sql"));
468
+ }
469
+ const lockKey = `${dbName}\0${options.namespace}`;
470
+ const existing = this.migrationLocks.get(lockKey);
471
+ if (existing) {
472
+ return existing;
473
+ }
474
+ const promise = this.applyMigrationsOnDbUnlocked(dbName, options);
475
+ this.migrationLocks.set(lockKey, promise);
476
+ try {
477
+ return await promise;
478
+ } finally {
479
+ this.migrationLocks.delete(lockKey);
480
+ }
481
+ }
403
482
  // === Internal methods called by DatabaseHandle ===
404
483
  async queryOnDb(dbName, sql, params, options) {
405
484
  return this.withTelemetry("query", dbName, async () => {
@@ -437,9 +516,15 @@ var SQLService = class extends BaseService {
437
516
  if (options?.schema) {
438
517
  body.schema = options.schema;
439
518
  }
519
+ const actions = [
520
+ this.actionForSql(sql, SQLAction.WRITE),
521
+ ...(options?.schema ?? []).map(
522
+ (statement) => this.actionForSql(statement, SQLAction.DDL)
523
+ )
524
+ ];
440
525
  const response = await this.invokeSQL(
441
526
  dbName,
442
- this.actionForSql(sql, SQLAction.WRITE),
527
+ this.dedupeActions(actions),
443
528
  body,
444
529
  options?.signal
445
530
  );
@@ -461,7 +546,7 @@ var SQLService = class extends BaseService {
461
546
  try {
462
547
  const response = await this.invokeSQL(
463
548
  dbName,
464
- this.actionForSqlBatch(statements),
549
+ this.actionsForSqlBatch(statements),
465
550
  { action: "batch", statements },
466
551
  options?.signal
467
552
  );
@@ -524,10 +609,83 @@ var SQLService = class extends BaseService {
524
609
  }
525
610
  });
526
611
  }
612
+ async applyMigrationsOnDbUnlocked(dbName, options) {
613
+ return this.withTelemetry("migrations.apply", dbName, async () => {
614
+ const created = await this.ensureMigrationsTable(dbName, options.signal);
615
+ if (!created.ok) return created;
616
+ const listed = await this.queryOnDb(
617
+ dbName,
618
+ `SELECT id FROM ${MIGRATIONS_TABLE} WHERE namespace = ? ORDER BY applied_at, id`,
619
+ [options.namespace],
620
+ { signal: options.signal }
621
+ );
622
+ if (!listed.ok) return listed;
623
+ const appliedIds = new Set(
624
+ listed.data.rows.map((row) => rowValue(row, 0)).filter((id) => typeof id === "string")
625
+ );
626
+ const skipped = options.migrations.filter((migration) => appliedIds.has(migration.id)).map((migration) => migration.id);
627
+ const pending = options.migrations.filter((migration) => !appliedIds.has(migration.id));
628
+ const applied = [];
629
+ for (const migration of pending) {
630
+ const result = await this.applyOneMigration(dbName, options.namespace, migration, options.signal);
631
+ if (!result.ok) return result;
632
+ applied.push(migration.id);
633
+ }
634
+ return ok({
635
+ database: dbName,
636
+ namespace: options.namespace,
637
+ status: applied.length > 0 ? "applied" : "already_current",
638
+ applied,
639
+ skipped
640
+ });
641
+ });
642
+ }
643
+ async applyOneMigration(dbName, namespace, migration, signal) {
644
+ const statements = [
645
+ ...migration.sql.map(
646
+ (statement) => typeof statement === "string" ? { sql: statement } : statement
647
+ ),
648
+ {
649
+ sql: `INSERT OR REPLACE INTO ${MIGRATIONS_TABLE} (key, namespace, id, applied_at, statement_count) VALUES (?, ?, ?, ?, ?)`,
650
+ params: [
651
+ migrationKey(namespace, migration.id),
652
+ namespace,
653
+ migration.id,
654
+ (/* @__PURE__ */ new Date()).toISOString(),
655
+ migration.sql.length
656
+ ]
657
+ }
658
+ ];
659
+ const result = await this.batchOnDb(dbName, statements, { signal });
660
+ if (!result.ok) return result;
661
+ return ok(void 0);
662
+ }
663
+ async ensureMigrationsTable(dbName, signal) {
664
+ const result = await this.batchOnDb(
665
+ dbName,
666
+ [
667
+ { sql: MIGRATIONS_SCHEMA },
668
+ {
669
+ sql: `INSERT OR REPLACE INTO ${MIGRATIONS_TABLE} (key, namespace, id, applied_at, statement_count) VALUES (?, ?, ?, ?, ?)`,
670
+ params: [
671
+ migrationKey(MIGRATIONS_META_NAMESPACE, MIGRATIONS_META_ID),
672
+ MIGRATIONS_META_NAMESPACE,
673
+ MIGRATIONS_META_ID,
674
+ (/* @__PURE__ */ new Date()).toISOString(),
675
+ 1
676
+ ]
677
+ }
678
+ ],
679
+ { signal }
680
+ );
681
+ if (!result.ok) return result;
682
+ return ok(void 0);
683
+ }
527
684
  // === Private helpers ===
528
- async invokeSQL(dbName, action, body, signal) {
685
+ async invokeSQL(dbName, actions, body, signal) {
529
686
  const session = this.context.session;
530
- const headers = this.context.invoke(session, "sql", dbName, action);
687
+ const actionList = Array.isArray(actions) ? actions : [actions];
688
+ const headers = actionList.length === 1 ? this.context.invoke(session, "sql", dbName, actionList[0]) : this.invokeSQLAny(session, dbName, actionList);
531
689
  return this.context.fetch(`${this.host}/invoke`, {
532
690
  method: "POST",
533
691
  headers: {
@@ -539,26 +697,50 @@ var SQLService = class extends BaseService {
539
697
  });
540
698
  }
541
699
  actionForSql(sql, fallback) {
542
- return firstSqlToken(sql) === "pragma" ? SQLAction.ADMIN : fallback;
700
+ const token = firstSqlToken(sql);
701
+ if (token === "pragma") return SQLAction.ADMIN;
702
+ if (token !== void 0 && DDL_TOKENS.has(token)) return SQLAction.DDL;
703
+ return fallback;
704
+ }
705
+ actionsForSqlBatch(statements) {
706
+ return this.dedupeActions(
707
+ statements.map((statement) => this.actionForSql(statement.sql, SQLAction.WRITE))
708
+ );
709
+ }
710
+ dedupeActions(actions) {
711
+ return [...new Set(actions)];
543
712
  }
544
- actionForSqlBatch(statements) {
545
- return statements.some(
546
- (statement) => this.actionForSql(statement.sql, SQLAction.WRITE) === SQLAction.ADMIN
547
- ) ? SQLAction.ADMIN : SQLAction.WRITE;
713
+ invokeSQLAny(session, dbName, actions) {
714
+ if (!this.context.invokeAny) {
715
+ throw new Error(
716
+ `SQL operation requires multiple permissions (${actions.join(", ")}) but this SDK runtime does not support multi-resource invocations`
717
+ );
718
+ }
719
+ return this.context.invokeAny(
720
+ session,
721
+ actions.map((action) => ({
722
+ spaceId: session.spaceId,
723
+ service: "sql",
724
+ path: dbName,
725
+ action
726
+ }))
727
+ );
548
728
  }
549
729
  async handleErrorResponse(response, operation) {
550
730
  const errorText = await response.text();
551
- let errorBody = {};
552
- try {
553
- errorBody = JSON.parse(errorText);
554
- } catch {
555
- }
731
+ const errorBody = parseServiceErrorBody(errorText);
556
732
  const errorCode = this.mapHttpStatusToErrorCode(
557
733
  response.status,
558
734
  errorBody.error
559
735
  );
560
- const message = errorBody.message || `SQL ${operation} failed: ${response.status} - ${errorText}`;
561
- const meta = { status: response.status, statusText: response.statusText };
736
+ const message = formatServiceResponseError(
737
+ "SQL",
738
+ operation,
739
+ response.status,
740
+ errorText,
741
+ errorBody
742
+ );
743
+ const meta = responseErrorMeta(response.status, response.statusText, errorText);
562
744
  if (response.status === 401) {
563
745
  const { resource, action } = parseAuthError(errorText);
564
746
  if (action) meta.requiredAction = action;
@@ -618,9 +800,57 @@ function firstSqlToken(sql) {
618
800
  const match = /^[A-Za-z_]+/.exec(sql.slice(index));
619
801
  return match?.[0].toLowerCase();
620
802
  }
803
+ function validateMigrationOptions(options) {
804
+ if (!options || typeof options !== "object") {
805
+ return "SQL migrations require options";
806
+ }
807
+ if (!options.namespace || typeof options.namespace !== "string") {
808
+ return "SQL migrations require a non-empty namespace";
809
+ }
810
+ if (!Array.isArray(options.migrations)) {
811
+ return "SQL migrations require an ordered migrations array";
812
+ }
813
+ const ids = /* @__PURE__ */ new Set();
814
+ for (const migration of options.migrations) {
815
+ if (!migration.id || typeof migration.id !== "string") {
816
+ return "SQL migrations require every migration to have a non-empty id";
817
+ }
818
+ if (ids.has(migration.id)) {
819
+ return `Duplicate SQL migration id: ${migration.id}`;
820
+ }
821
+ ids.add(migration.id);
822
+ if (!Array.isArray(migration.sql)) {
823
+ return `SQL migration ${migration.id} requires a sql array`;
824
+ }
825
+ for (const statement of migration.sql) {
826
+ if (typeof statement === "string") {
827
+ if (statement.trim() === "") {
828
+ return `SQL migration ${migration.id} contains an empty statement`;
829
+ }
830
+ continue;
831
+ }
832
+ if (!statement || typeof statement.sql !== "string" || statement.sql.trim() === "") {
833
+ return `SQL migration ${migration.id} contains an invalid statement`;
834
+ }
835
+ }
836
+ }
837
+ return null;
838
+ }
839
+ function migrationKey(namespace, id) {
840
+ return `${encodeURIComponent(namespace)}:${encodeURIComponent(id)}`;
841
+ }
842
+ function rowValue(row, index) {
843
+ if (Array.isArray(row)) return row[index];
844
+ if (row && typeof row === "object") {
845
+ const values = Object.values(row);
846
+ return values[index];
847
+ }
848
+ return void 0;
849
+ }
621
850
  export {
622
851
  DatabaseHandle,
623
852
  SQLAction,
853
+ SQLMigrations,
624
854
  SQLService
625
855
  };
626
856
  //# sourceMappingURL=index.js.map