@tinycloud/sdk-services 2.3.0 → 2.4.0-beta.10
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 +326 -52
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +325 -52
- package/dist/index.js.map +1 -1
- package/dist/kv/index.cjs +70 -29
- package/dist/kv/index.cjs.map +1 -1
- package/dist/kv/index.d.cts +28 -0
- package/dist/kv/index.d.ts +28 -0
- package/dist/kv/index.js +70 -29
- package/dist/kv/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/sql/index.d.cts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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,
|
|
685
|
+
async invokeSQL(dbName, actions, body, signal) {
|
|
529
686
|
const session = this.context.session;
|
|
530
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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
|
-
|
|
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 =
|
|
561
|
-
|
|
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
|