@tinycloud/sdk-services 2.4.0-beta.8 → 2.4.0
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/sql/index.d.cts
CHANGED
|
@@ -85,13 +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";
|
|
94
|
-
readonly
|
|
135
|
+
readonly SCHEMA: "tinycloud.sql/schema";
|
|
95
136
|
readonly ADMIN: "tinycloud.sql/admin";
|
|
96
137
|
readonly SELECT: "tinycloud.sql/select";
|
|
97
138
|
readonly INSERT: "tinycloud.sql/insert";
|
|
@@ -109,12 +150,23 @@ type SQLActionType = (typeof SQLAction)[keyof typeof SQLAction];
|
|
|
109
150
|
* Platform-agnostic interface for SQL database operations.
|
|
110
151
|
*/
|
|
111
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
|
+
}
|
|
112
162
|
/**
|
|
113
163
|
* Database handle interface for operations on a specific named database.
|
|
114
164
|
*/
|
|
115
165
|
interface IDatabaseHandle {
|
|
116
166
|
/** The database name */
|
|
117
167
|
readonly name: string;
|
|
168
|
+
/** Versioned schema migration helper. */
|
|
169
|
+
readonly migrations: ISQLMigrations;
|
|
118
170
|
/**
|
|
119
171
|
* Execute a SQL query and return rows.
|
|
120
172
|
*/
|
|
@@ -179,6 +231,7 @@ interface ISQLService extends IService {
|
|
|
179
231
|
declare class SQLService extends BaseService implements ISQLService {
|
|
180
232
|
static readonly serviceName = "sql";
|
|
181
233
|
protected _config: SQLServiceConfig;
|
|
234
|
+
private readonly migrationLocks;
|
|
182
235
|
constructor(config?: SQLServiceConfig);
|
|
183
236
|
get config(): SQLServiceConfig;
|
|
184
237
|
private get defaultDbName();
|
|
@@ -199,11 +252,15 @@ declare class SQLService extends BaseService implements ISQLService {
|
|
|
199
252
|
* Shortcut: batch on the default database.
|
|
200
253
|
*/
|
|
201
254
|
batch(statements: SqlStatement[], options?: BatchOptions): Promise<Result<BatchResponse>>;
|
|
255
|
+
applyMigrationsOnDb(dbName: string, options: SqlMigrationApplyOptions): Promise<Result<SqlMigrationApplyResponse>>;
|
|
202
256
|
queryOnDb<T = Record<string, unknown>>(dbName: string, sql: string, params?: SqlValue[], options?: QueryOptions): Promise<Result<QueryResponse<T>>>;
|
|
203
257
|
executeOnDb(dbName: string, sql: string, params?: SqlValue[], options?: ExecuteOptions): Promise<Result<ExecuteResponse>>;
|
|
204
258
|
batchOnDb(dbName: string, statements: SqlStatement[], options?: BatchOptions): Promise<Result<BatchResponse>>;
|
|
205
259
|
executeStatementOnDb(dbName: string, name: string, params?: SqlValue[], options?: QueryOptions): Promise<Result<QueryResponse | ExecuteResponse>>;
|
|
206
260
|
exportDb(dbName: string, options?: QueryOptions): Promise<Result<Blob>>;
|
|
261
|
+
private applyMigrationsOnDbUnlocked;
|
|
262
|
+
private applyOneMigration;
|
|
263
|
+
private ensureMigrationsTable;
|
|
207
264
|
private invokeSQL;
|
|
208
265
|
private actionForSql;
|
|
209
266
|
private actionsForSqlBatch;
|
|
@@ -222,6 +279,7 @@ declare class SQLService extends BaseService implements ISQLService {
|
|
|
222
279
|
declare class DatabaseHandle implements IDatabaseHandle {
|
|
223
280
|
private service;
|
|
224
281
|
readonly name: string;
|
|
282
|
+
readonly migrations: ISQLMigrations;
|
|
225
283
|
constructor(service: SQLService, name: string);
|
|
226
284
|
query<T = Record<string, unknown>>(sql: string, params?: SqlValue[], options?: QueryOptions): Promise<Result<QueryResponse<T>>>;
|
|
227
285
|
execute(sql: string, params?: SqlValue[], options?: ExecuteOptions): Promise<Result<ExecuteResponse>>;
|
|
@@ -230,4 +288,11 @@ declare class DatabaseHandle implements IDatabaseHandle {
|
|
|
230
288
|
export(options?: QueryOptions): Promise<Result<Blob>>;
|
|
231
289
|
}
|
|
232
290
|
|
|
233
|
-
|
|
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,13 +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";
|
|
94
|
-
readonly
|
|
135
|
+
readonly SCHEMA: "tinycloud.sql/schema";
|
|
95
136
|
readonly ADMIN: "tinycloud.sql/admin";
|
|
96
137
|
readonly SELECT: "tinycloud.sql/select";
|
|
97
138
|
readonly INSERT: "tinycloud.sql/insert";
|
|
@@ -109,12 +150,23 @@ type SQLActionType = (typeof SQLAction)[keyof typeof SQLAction];
|
|
|
109
150
|
* Platform-agnostic interface for SQL database operations.
|
|
110
151
|
*/
|
|
111
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
|
+
}
|
|
112
162
|
/**
|
|
113
163
|
* Database handle interface for operations on a specific named database.
|
|
114
164
|
*/
|
|
115
165
|
interface IDatabaseHandle {
|
|
116
166
|
/** The database name */
|
|
117
167
|
readonly name: string;
|
|
168
|
+
/** Versioned schema migration helper. */
|
|
169
|
+
readonly migrations: ISQLMigrations;
|
|
118
170
|
/**
|
|
119
171
|
* Execute a SQL query and return rows.
|
|
120
172
|
*/
|
|
@@ -179,6 +231,7 @@ interface ISQLService extends IService {
|
|
|
179
231
|
declare class SQLService extends BaseService implements ISQLService {
|
|
180
232
|
static readonly serviceName = "sql";
|
|
181
233
|
protected _config: SQLServiceConfig;
|
|
234
|
+
private readonly migrationLocks;
|
|
182
235
|
constructor(config?: SQLServiceConfig);
|
|
183
236
|
get config(): SQLServiceConfig;
|
|
184
237
|
private get defaultDbName();
|
|
@@ -199,11 +252,15 @@ declare class SQLService extends BaseService implements ISQLService {
|
|
|
199
252
|
* Shortcut: batch on the default database.
|
|
200
253
|
*/
|
|
201
254
|
batch(statements: SqlStatement[], options?: BatchOptions): Promise<Result<BatchResponse>>;
|
|
255
|
+
applyMigrationsOnDb(dbName: string, options: SqlMigrationApplyOptions): Promise<Result<SqlMigrationApplyResponse>>;
|
|
202
256
|
queryOnDb<T = Record<string, unknown>>(dbName: string, sql: string, params?: SqlValue[], options?: QueryOptions): Promise<Result<QueryResponse<T>>>;
|
|
203
257
|
executeOnDb(dbName: string, sql: string, params?: SqlValue[], options?: ExecuteOptions): Promise<Result<ExecuteResponse>>;
|
|
204
258
|
batchOnDb(dbName: string, statements: SqlStatement[], options?: BatchOptions): Promise<Result<BatchResponse>>;
|
|
205
259
|
executeStatementOnDb(dbName: string, name: string, params?: SqlValue[], options?: QueryOptions): Promise<Result<QueryResponse | ExecuteResponse>>;
|
|
206
260
|
exportDb(dbName: string, options?: QueryOptions): Promise<Result<Blob>>;
|
|
261
|
+
private applyMigrationsOnDbUnlocked;
|
|
262
|
+
private applyOneMigration;
|
|
263
|
+
private ensureMigrationsTable;
|
|
207
264
|
private invokeSQL;
|
|
208
265
|
private actionForSql;
|
|
209
266
|
private actionsForSqlBatch;
|
|
@@ -222,6 +279,7 @@ declare class SQLService extends BaseService implements ISQLService {
|
|
|
222
279
|
declare class DatabaseHandle implements IDatabaseHandle {
|
|
223
280
|
private service;
|
|
224
281
|
readonly name: string;
|
|
282
|
+
readonly migrations: ISQLMigrations;
|
|
225
283
|
constructor(service: SQLService, name: string);
|
|
226
284
|
query<T = Record<string, unknown>>(sql: string, params?: SqlValue[], options?: QueryOptions): Promise<Result<QueryResponse<T>>>;
|
|
227
285
|
execute(sql: string, params?: SqlValue[], options?: ExecuteOptions): Promise<Result<ExecuteResponse>>;
|
|
@@ -230,4 +288,11 @@ declare class DatabaseHandle implements IDatabaseHandle {
|
|
|
230
288
|
export(options?: QueryOptions): Promise<Result<Blob>>;
|
|
231
289
|
}
|
|
232
290
|
|
|
233
|
-
|
|
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,7 +399,7 @@ var DatabaseHandle = class {
|
|
|
351
399
|
var SQLAction = {
|
|
352
400
|
READ: "tinycloud.sql/read",
|
|
353
401
|
WRITE: "tinycloud.sql/write",
|
|
354
|
-
|
|
402
|
+
SCHEMA: "tinycloud.sql/schema",
|
|
355
403
|
ADMIN: "tinycloud.sql/admin",
|
|
356
404
|
SELECT: "tinycloud.sql/select",
|
|
357
405
|
INSERT: "tinycloud.sql/insert",
|
|
@@ -364,9 +412,20 @@ var SQLAction = {
|
|
|
364
412
|
|
|
365
413
|
// src/sql/SQLService.ts
|
|
366
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";
|
|
367
425
|
var SQLService = class extends BaseService {
|
|
368
426
|
constructor(config = {}) {
|
|
369
427
|
super();
|
|
428
|
+
this.migrationLocks = /* @__PURE__ */ new Map();
|
|
370
429
|
this._config = config;
|
|
371
430
|
}
|
|
372
431
|
get config() {
|
|
@@ -402,6 +461,24 @@ var SQLService = class extends BaseService {
|
|
|
402
461
|
async batch(statements, options) {
|
|
403
462
|
return this.batchOnDb(this.defaultDbName, statements, options);
|
|
404
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
|
+
}
|
|
405
482
|
// === Internal methods called by DatabaseHandle ===
|
|
406
483
|
async queryOnDb(dbName, sql, params, options) {
|
|
407
484
|
return this.withTelemetry("query", dbName, async () => {
|
|
@@ -442,7 +519,7 @@ var SQLService = class extends BaseService {
|
|
|
442
519
|
const actions = [
|
|
443
520
|
this.actionForSql(sql, SQLAction.WRITE),
|
|
444
521
|
...(options?.schema ?? []).map(
|
|
445
|
-
(statement) => this.actionForSql(statement, SQLAction.
|
|
522
|
+
(statement) => this.actionForSql(statement, SQLAction.SCHEMA)
|
|
446
523
|
)
|
|
447
524
|
];
|
|
448
525
|
const response = await this.invokeSQL(
|
|
@@ -532,6 +609,78 @@ var SQLService = class extends BaseService {
|
|
|
532
609
|
}
|
|
533
610
|
});
|
|
534
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
|
+
}
|
|
535
684
|
// === Private helpers ===
|
|
536
685
|
async invokeSQL(dbName, actions, body, signal) {
|
|
537
686
|
const session = this.context.session;
|
|
@@ -550,7 +699,7 @@ var SQLService = class extends BaseService {
|
|
|
550
699
|
actionForSql(sql, fallback) {
|
|
551
700
|
const token = firstSqlToken(sql);
|
|
552
701
|
if (token === "pragma") return SQLAction.ADMIN;
|
|
553
|
-
if (token !== void 0 && DDL_TOKENS.has(token)) return SQLAction.
|
|
702
|
+
if (token !== void 0 && DDL_TOKENS.has(token)) return SQLAction.SCHEMA;
|
|
554
703
|
return fallback;
|
|
555
704
|
}
|
|
556
705
|
actionsForSqlBatch(statements) {
|
|
@@ -579,17 +728,19 @@ var SQLService = class extends BaseService {
|
|
|
579
728
|
}
|
|
580
729
|
async handleErrorResponse(response, operation) {
|
|
581
730
|
const errorText = await response.text();
|
|
582
|
-
|
|
583
|
-
try {
|
|
584
|
-
errorBody = JSON.parse(errorText);
|
|
585
|
-
} catch {
|
|
586
|
-
}
|
|
731
|
+
const errorBody = parseServiceErrorBody(errorText);
|
|
587
732
|
const errorCode = this.mapHttpStatusToErrorCode(
|
|
588
733
|
response.status,
|
|
589
734
|
errorBody.error
|
|
590
735
|
);
|
|
591
|
-
const message =
|
|
592
|
-
|
|
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);
|
|
593
744
|
if (response.status === 401) {
|
|
594
745
|
const { resource, action } = parseAuthError(errorText);
|
|
595
746
|
if (action) meta.requiredAction = action;
|
|
@@ -649,9 +800,57 @@ function firstSqlToken(sql) {
|
|
|
649
800
|
const match = /^[A-Za-z_]+/.exec(sql.slice(index));
|
|
650
801
|
return match?.[0].toLowerCase();
|
|
651
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
|
+
}
|
|
652
850
|
export {
|
|
653
851
|
DatabaseHandle,
|
|
654
852
|
SQLAction,
|
|
853
|
+
SQLMigrations,
|
|
655
854
|
SQLService
|
|
656
855
|
};
|
|
657
856
|
//# sourceMappingURL=index.js.map
|