@aouda/client 0.1.9 → 0.1.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.
- package/dist/cli/index.cjs +241 -9
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.d.cts +1 -1
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +241 -9
- package/dist/cli/index.js.map +1 -1
- package/dist/{client-CxP9Vnc8.d.cts → client-CT3GdrvA.d.cts} +111 -5
- package/dist/{client-CxP9Vnc8.d.ts → client-CT3GdrvA.d.ts} +111 -5
- package/dist/index.cjs +246 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +243 -8
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.d.cts
CHANGED
package/dist/cli/index.d.ts
CHANGED
package/dist/cli/index.js
CHANGED
|
@@ -392,7 +392,7 @@ import { fileURLToPath } from "url";
|
|
|
392
392
|
// package.json
|
|
393
393
|
var package_default = {
|
|
394
394
|
name: "@aouda/client",
|
|
395
|
-
version: "0.1.
|
|
395
|
+
version: "0.1.11",
|
|
396
396
|
description: "Official TypeScript/JavaScript client library for Aouda",
|
|
397
397
|
type: "module",
|
|
398
398
|
main: "./dist/index.cjs",
|
|
@@ -630,6 +630,7 @@ var AuthHandler = class {
|
|
|
630
630
|
this._tokenExpiry = null;
|
|
631
631
|
this._authenticated = false;
|
|
632
632
|
this._refreshPromise = null;
|
|
633
|
+
this._onAccessTokenRefreshed = null;
|
|
633
634
|
this._serverUrl = serverUrl.replace(/\/+$/, "");
|
|
634
635
|
this._timeout = timeout;
|
|
635
636
|
this._authBasePath = config.basePath;
|
|
@@ -879,10 +880,16 @@ var AuthHandler = class {
|
|
|
879
880
|
const thresholdSeconds = this._refreshThresholdMs / 1e3;
|
|
880
881
|
return this._tokenExpiry <= nowSeconds + thresholdSeconds;
|
|
881
882
|
}
|
|
883
|
+
setOnAccessTokenRefreshed(handler) {
|
|
884
|
+
this._onAccessTokenRefreshed = handler;
|
|
885
|
+
}
|
|
882
886
|
async _doRefresh() {
|
|
883
887
|
try {
|
|
884
888
|
const result = await this.callRefreshEndpoint();
|
|
885
889
|
this.handleSignInResult(result);
|
|
890
|
+
if (result.accessToken) {
|
|
891
|
+
this._onAccessTokenRefreshed?.(result.accessToken);
|
|
892
|
+
}
|
|
886
893
|
return true;
|
|
887
894
|
} catch {
|
|
888
895
|
return false;
|
|
@@ -1483,7 +1490,15 @@ var ERROR_CODE_MAP = {
|
|
|
1483
1490
|
AUTH_TOKEN_INVALID: AoudaAuthenticationError,
|
|
1484
1491
|
AUTH_TOKEN_REVOKED: AoudaAuthenticationError,
|
|
1485
1492
|
AUTH_API_KEY_INVALID: AoudaAuthenticationError,
|
|
1486
|
-
AUTH_REQUIRED: AoudaAuthenticationError
|
|
1493
|
+
AUTH_REQUIRED: AoudaAuthenticationError,
|
|
1494
|
+
NAMED_QUERY_NOT_FOUND: AoudaNotFoundError,
|
|
1495
|
+
NAMED_MUTATION_NOT_FOUND: AoudaNotFoundError,
|
|
1496
|
+
NAMED_QUERY_BATCH_EMPTY: AoudaValidationError,
|
|
1497
|
+
NAMED_QUERY_BATCH_TOO_LARGE: AoudaValidationError,
|
|
1498
|
+
NAMED_QUERY_BATCH_MUTATION: AoudaValidationError,
|
|
1499
|
+
NAMED_QUERY_BIND_FAILED: AoudaValidationError,
|
|
1500
|
+
NAMED_QUERY_PARAM_REQUIRED: AoudaValidationError,
|
|
1501
|
+
NAMED_MUTATION_BIND_FAILED: AoudaValidationError
|
|
1487
1502
|
};
|
|
1488
1503
|
function createComposedAbortController(...signals) {
|
|
1489
1504
|
const controller = new AbortController();
|
|
@@ -2376,6 +2391,8 @@ var TableSubscription = class {
|
|
|
2376
2391
|
this._started = false;
|
|
2377
2392
|
this._startPromise = null;
|
|
2378
2393
|
this._lastVersion = 0;
|
|
2394
|
+
this._pendingSnapshotRows = [];
|
|
2395
|
+
this._forceFreshSubscribe = false;
|
|
2379
2396
|
this.id = createStreamingId("sub");
|
|
2380
2397
|
this._transport = transport;
|
|
2381
2398
|
this._tableName = tableName;
|
|
@@ -2383,6 +2400,7 @@ var TableSubscription = class {
|
|
|
2383
2400
|
this._onSnapshot = options.onSnapshot;
|
|
2384
2401
|
this._onChange = options.onChange;
|
|
2385
2402
|
this._onError = options.onError;
|
|
2403
|
+
this._conflate = options.conflate;
|
|
2386
2404
|
this._reconnectHandlerKey = `${this.id}::reconnect`;
|
|
2387
2405
|
}
|
|
2388
2406
|
get lastVersion() {
|
|
@@ -2432,7 +2450,9 @@ var TableSubscription = class {
|
|
|
2432
2450
|
return;
|
|
2433
2451
|
}
|
|
2434
2452
|
try {
|
|
2435
|
-
|
|
2453
|
+
this._pendingSnapshotRows = [];
|
|
2454
|
+
const resumeFrom = this._forceFreshSubscribe || this._lastVersion <= 0 ? void 0 : this._lastVersion;
|
|
2455
|
+
this._forceFreshSubscribe = false;
|
|
2436
2456
|
await this._sendSubscribe(resumeFrom);
|
|
2437
2457
|
} catch (error) {
|
|
2438
2458
|
const wrapped = error instanceof Error ? error : new AoudaConnectionError("Subscription reconnect failed");
|
|
@@ -2452,6 +2472,9 @@ var TableSubscription = class {
|
|
|
2452
2472
|
if (resumeFrom !== void 0) {
|
|
2453
2473
|
message.resume_from = resumeFrom;
|
|
2454
2474
|
}
|
|
2475
|
+
if (this._conflate !== void 0) {
|
|
2476
|
+
message.conflate = this._conflate;
|
|
2477
|
+
}
|
|
2455
2478
|
await this._transport.send(message);
|
|
2456
2479
|
}
|
|
2457
2480
|
_handleMessage(message) {
|
|
@@ -2460,7 +2483,13 @@ var TableSubscription = class {
|
|
|
2460
2483
|
}
|
|
2461
2484
|
switch (message.type) {
|
|
2462
2485
|
case "snapshot":
|
|
2463
|
-
this.
|
|
2486
|
+
this._handleSnapshotPage(message);
|
|
2487
|
+
return;
|
|
2488
|
+
case "snapshot_complete":
|
|
2489
|
+
this._handleSnapshotComplete(message);
|
|
2490
|
+
return;
|
|
2491
|
+
case "gap":
|
|
2492
|
+
void this._handleGap(message);
|
|
2464
2493
|
return;
|
|
2465
2494
|
case "change":
|
|
2466
2495
|
this._handleChange(message);
|
|
@@ -2472,9 +2501,13 @@ var TableSubscription = class {
|
|
|
2472
2501
|
return;
|
|
2473
2502
|
}
|
|
2474
2503
|
}
|
|
2475
|
-
|
|
2504
|
+
_handleSnapshotPage(message) {
|
|
2505
|
+
this._pendingSnapshotRows.push(...message.rows);
|
|
2506
|
+
}
|
|
2507
|
+
_handleSnapshotComplete(message) {
|
|
2476
2508
|
this._lastVersion = message.version;
|
|
2477
|
-
const rows =
|
|
2509
|
+
const rows = this._pendingSnapshotRows;
|
|
2510
|
+
this._pendingSnapshotRows = [];
|
|
2478
2511
|
this._onSnapshot?.(rows, message.version);
|
|
2479
2512
|
this._queue.push({
|
|
2480
2513
|
type: "snapshot",
|
|
@@ -2482,6 +2515,16 @@ var TableSubscription = class {
|
|
|
2482
2515
|
version: message.version
|
|
2483
2516
|
});
|
|
2484
2517
|
}
|
|
2518
|
+
async _handleGap(message) {
|
|
2519
|
+
this._pendingSnapshotRows = [];
|
|
2520
|
+
this._lastVersion = message.last_seq;
|
|
2521
|
+
try {
|
|
2522
|
+
await this._sendSubscribe(message.last_seq);
|
|
2523
|
+
} catch (error) {
|
|
2524
|
+
const wrapped = error instanceof Error ? error : new AoudaConnectionError("Subscription gap resume failed");
|
|
2525
|
+
this._notifyError(wrapped);
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2485
2528
|
_handleChange(message) {
|
|
2486
2529
|
this._lastVersion = message.version;
|
|
2487
2530
|
const event = {
|
|
@@ -2492,10 +2535,20 @@ var TableSubscription = class {
|
|
|
2492
2535
|
key: message.key,
|
|
2493
2536
|
version: message.version
|
|
2494
2537
|
};
|
|
2538
|
+
if (message.values_skipped !== void 0) {
|
|
2539
|
+
event.values_skipped = message.values_skipped;
|
|
2540
|
+
}
|
|
2495
2541
|
this._onChange?.(event);
|
|
2496
2542
|
this._queue.push(event);
|
|
2497
2543
|
}
|
|
2498
2544
|
_handleServerError(message) {
|
|
2545
|
+
if (message.code === "SLOW_CONSUMER") {
|
|
2546
|
+
this._pendingSnapshotRows = [];
|
|
2547
|
+
this._lastVersion = 0;
|
|
2548
|
+
this._forceFreshSubscribe = true;
|
|
2549
|
+
return;
|
|
2550
|
+
}
|
|
2551
|
+
this._pendingSnapshotRows = [];
|
|
2499
2552
|
const error = new AoudaConnectionError(
|
|
2500
2553
|
`Subscription error (${message.code}): ${message.message}`
|
|
2501
2554
|
);
|
|
@@ -3208,7 +3261,8 @@ var TableQuery = class _TableQuery {
|
|
|
3208
3261
|
{
|
|
3209
3262
|
onSnapshot: options.onSnapshot,
|
|
3210
3263
|
onChange: options.onChange,
|
|
3211
|
-
onError: options.onError
|
|
3264
|
+
onError: options.onError,
|
|
3265
|
+
conflate: options.conflate
|
|
3212
3266
|
},
|
|
3213
3267
|
mergedFilter
|
|
3214
3268
|
);
|
|
@@ -4903,6 +4957,147 @@ var MaterializedQueriesApi = class {
|
|
|
4903
4957
|
}
|
|
4904
4958
|
};
|
|
4905
4959
|
|
|
4960
|
+
// src/named-queries.ts
|
|
4961
|
+
var MAX_NAMED_QUERY_BATCH_SIZE = 32;
|
|
4962
|
+
function raiseDeprecationWarnings(sink, warnings) {
|
|
4963
|
+
if (warnings == null) return;
|
|
4964
|
+
for (const warning of warnings) {
|
|
4965
|
+
if (warning.code !== "NAMED_QUERY_DEPRECATED" && warning.code !== "NAMED_MUTATION_DEPRECATED") {
|
|
4966
|
+
continue;
|
|
4967
|
+
}
|
|
4968
|
+
const sunset = warning.sunsetAt != null ? ` sunsetAt=${warning.sunsetAt}` : "";
|
|
4969
|
+
const hash = warning.hash != null && warning.hash.length > 0 ? ` hash=${warning.hash}` : "";
|
|
4970
|
+
sink({
|
|
4971
|
+
code: warning.code,
|
|
4972
|
+
hash: warning.hash,
|
|
4973
|
+
sunsetAt: warning.sunsetAt,
|
|
4974
|
+
message: `${warning.code}:${hash}${sunset}`.trim()
|
|
4975
|
+
});
|
|
4976
|
+
}
|
|
4977
|
+
}
|
|
4978
|
+
function emptyStats() {
|
|
4979
|
+
return {
|
|
4980
|
+
rowsScanned: 0,
|
|
4981
|
+
rowsReturned: 0,
|
|
4982
|
+
segmentsAccessed: 0,
|
|
4983
|
+
executionMs: 0
|
|
4984
|
+
};
|
|
4985
|
+
}
|
|
4986
|
+
var NamedQueriesApi = class {
|
|
4987
|
+
constructor(transport, database, onWarning) {
|
|
4988
|
+
this.transport = transport;
|
|
4989
|
+
this.database = database;
|
|
4990
|
+
this.onWarning = onWarning;
|
|
4991
|
+
}
|
|
4992
|
+
async execute(hash, args, options) {
|
|
4993
|
+
if (typeof hash !== "string" || hash.trim().length === 0) {
|
|
4994
|
+
throw new Error("Named query hash must be a non-empty string");
|
|
4995
|
+
}
|
|
4996
|
+
const prefix = databasePath2(this.database);
|
|
4997
|
+
const path4 = `${prefix}/named-queries/${encodeURIComponent(hash)}/query?format=columnar`;
|
|
4998
|
+
const response = await this.transport.post(
|
|
4999
|
+
path4,
|
|
5000
|
+
{ args: args ?? {} },
|
|
5001
|
+
{ signal: options?.signal }
|
|
5002
|
+
);
|
|
5003
|
+
const rows = columnarToRows(response);
|
|
5004
|
+
raiseDeprecationWarnings(this.onWarning, response.warnings);
|
|
5005
|
+
return {
|
|
5006
|
+
rows,
|
|
5007
|
+
stats: response.stats,
|
|
5008
|
+
warnings: response.warnings
|
|
5009
|
+
};
|
|
5010
|
+
}
|
|
5011
|
+
async batch(items, options) {
|
|
5012
|
+
if (items.length === 0) {
|
|
5013
|
+
throw new AoudaValidationError(
|
|
5014
|
+
"Named query batch requires a non-empty queries array.",
|
|
5015
|
+
"NAMED_QUERY_BATCH_EMPTY",
|
|
5016
|
+
400
|
|
5017
|
+
);
|
|
5018
|
+
}
|
|
5019
|
+
if (items.length > MAX_NAMED_QUERY_BATCH_SIZE) {
|
|
5020
|
+
throw new AoudaValidationError(
|
|
5021
|
+
`Named query batch exceeds ${MAX_NAMED_QUERY_BATCH_SIZE} elements.`,
|
|
5022
|
+
"NAMED_QUERY_BATCH_TOO_LARGE",
|
|
5023
|
+
400
|
|
5024
|
+
);
|
|
5025
|
+
}
|
|
5026
|
+
const prefix = databasePath2(this.database);
|
|
5027
|
+
const path4 = `${prefix}/named-queries/batch?format=columnar`;
|
|
5028
|
+
const envelope = await this.transport.post(
|
|
5029
|
+
path4,
|
|
5030
|
+
{ queries: items },
|
|
5031
|
+
{ signal: options?.signal }
|
|
5032
|
+
);
|
|
5033
|
+
return (envelope.results ?? []).map((slot) => {
|
|
5034
|
+
if (slot.code != null && slot.code.length > 0) {
|
|
5035
|
+
return {
|
|
5036
|
+
isError: true,
|
|
5037
|
+
code: slot.code,
|
|
5038
|
+
error: slot.error
|
|
5039
|
+
};
|
|
5040
|
+
}
|
|
5041
|
+
const columnar = {
|
|
5042
|
+
columns: slot.columns ?? [],
|
|
5043
|
+
types: slot.types ?? [],
|
|
5044
|
+
data: slot.data ?? [],
|
|
5045
|
+
rowCount: slot.rowCount ?? 0,
|
|
5046
|
+
stats: slot.stats ?? emptyStats(),
|
|
5047
|
+
warnings: slot.warnings
|
|
5048
|
+
};
|
|
5049
|
+
const result = {
|
|
5050
|
+
rows: columnarToRows(columnar),
|
|
5051
|
+
stats: columnar.stats,
|
|
5052
|
+
warnings: slot.warnings
|
|
5053
|
+
};
|
|
5054
|
+
raiseDeprecationWarnings(this.onWarning, slot.warnings);
|
|
5055
|
+
return { isError: false, result };
|
|
5056
|
+
});
|
|
5057
|
+
}
|
|
5058
|
+
};
|
|
5059
|
+
var NamedMutationsApi = class {
|
|
5060
|
+
constructor(transport, database, onWarning) {
|
|
5061
|
+
this.transport = transport;
|
|
5062
|
+
this.database = database;
|
|
5063
|
+
this.onWarning = onWarning;
|
|
5064
|
+
}
|
|
5065
|
+
async execute(hash, args) {
|
|
5066
|
+
if (typeof hash !== "string" || hash.trim().length === 0) {
|
|
5067
|
+
throw new Error("Named mutation hash must be a non-empty string");
|
|
5068
|
+
}
|
|
5069
|
+
const prefix = databasePath2(this.database);
|
|
5070
|
+
const path4 = `${prefix}/named-mutations/${encodeURIComponent(hash)}/execute`;
|
|
5071
|
+
const wire = await this.transport.post(path4, {
|
|
5072
|
+
args: args ?? {}
|
|
5073
|
+
});
|
|
5074
|
+
let op = "unknown";
|
|
5075
|
+
let rowsAffected = 0;
|
|
5076
|
+
if (wire.rowsInserted != null) {
|
|
5077
|
+
op = "insert";
|
|
5078
|
+
rowsAffected = wire.rowsInserted;
|
|
5079
|
+
} else if (wire.rowsUpdated != null) {
|
|
5080
|
+
op = "update";
|
|
5081
|
+
rowsAffected = wire.rowsUpdated;
|
|
5082
|
+
} else if (wire.rowsDeleted != null) {
|
|
5083
|
+
op = "delete";
|
|
5084
|
+
rowsAffected = wire.rowsDeleted;
|
|
5085
|
+
}
|
|
5086
|
+
raiseDeprecationWarnings(this.onWarning, wire.warnings);
|
|
5087
|
+
const returning = wire.rows == null ? void 0 : {
|
|
5088
|
+
rows: columnarToRows(wire.rows),
|
|
5089
|
+
stats: wire.rows.stats,
|
|
5090
|
+
warnings: wire.rows.warnings
|
|
5091
|
+
};
|
|
5092
|
+
return {
|
|
5093
|
+
op,
|
|
5094
|
+
rowsAffected,
|
|
5095
|
+
returning,
|
|
5096
|
+
warnings: wire.warnings
|
|
5097
|
+
};
|
|
5098
|
+
}
|
|
5099
|
+
};
|
|
5100
|
+
|
|
4906
5101
|
// src/streaming/websocket-transport.ts
|
|
4907
5102
|
import { decode as decodeMessagePack, encode as encodeMessagePack } from "@msgpack/msgpack";
|
|
4908
5103
|
var DEFAULT_PING_INTERVAL_MS = 2e4;
|
|
@@ -4927,6 +5122,7 @@ var WebSocketTransport = class {
|
|
|
4927
5122
|
// Resolved/rejected when the auth handshake completes.
|
|
4928
5123
|
this._authResolve = null;
|
|
4929
5124
|
this._authReject = null;
|
|
5125
|
+
this._handshakeComplete = false;
|
|
4930
5126
|
this._serverUrl = serverUrl;
|
|
4931
5127
|
this._database = database;
|
|
4932
5128
|
this._authHandler = options.authHandler;
|
|
@@ -4936,6 +5132,11 @@ var WebSocketTransport = class {
|
|
|
4936
5132
|
this._maxMissedHeartbeats = options.maxMissedHeartbeats ?? DEFAULT_MAX_MISSED_HEARTBEATS;
|
|
4937
5133
|
this._enableCompression = options.enableCompression ?? true;
|
|
4938
5134
|
this._wireMode = options.wireMode ?? "json";
|
|
5135
|
+
this._authHandler?.setOnAccessTokenRefreshed((token) => {
|
|
5136
|
+
if (this._handshakeComplete && this._ws?.readyState === WS_READY_STATE_OPEN) {
|
|
5137
|
+
void this.send({ type: "re_auth", token });
|
|
5138
|
+
}
|
|
5139
|
+
});
|
|
4939
5140
|
}
|
|
4940
5141
|
// ─── Public API ────────────────────────────────────────────────────────────
|
|
4941
5142
|
/** Latest version number from the most recent server `heartbeat` message. */
|
|
@@ -5108,6 +5309,7 @@ var WebSocketTransport = class {
|
|
|
5108
5309
|
const resolve4 = this._authResolve;
|
|
5109
5310
|
this._authResolve = null;
|
|
5110
5311
|
this._authReject = null;
|
|
5312
|
+
this._handshakeComplete = true;
|
|
5111
5313
|
if (resolve4) resolve4();
|
|
5112
5314
|
break;
|
|
5113
5315
|
}
|
|
@@ -5134,10 +5336,15 @@ var WebSocketTransport = class {
|
|
|
5134
5336
|
break;
|
|
5135
5337
|
}
|
|
5136
5338
|
const id = "id" in msg ? msg.id : void 0;
|
|
5137
|
-
if (id !== void 0 && id !== null) {
|
|
5339
|
+
if (id !== void 0 && id !== null && id !== "") {
|
|
5138
5340
|
this._handlers.get(id)?.(msg);
|
|
5139
5341
|
} else {
|
|
5140
5342
|
this._handlers.get("__global__")?.(msg);
|
|
5343
|
+
for (const [key, handler] of this._handlers) {
|
|
5344
|
+
if (key !== "__global__") {
|
|
5345
|
+
handler(msg);
|
|
5346
|
+
}
|
|
5347
|
+
}
|
|
5141
5348
|
}
|
|
5142
5349
|
}
|
|
5143
5350
|
// ─── Internal: reconnect ──────────────────────────────────────────────────
|
|
@@ -5665,6 +5872,19 @@ var AoudaClient = class {
|
|
|
5665
5872
|
this.transport,
|
|
5666
5873
|
this.database
|
|
5667
5874
|
);
|
|
5875
|
+
const onNamedArtifactWarning = options.onNamedArtifactWarning ?? ((warning) => {
|
|
5876
|
+
console.warn(warning.message);
|
|
5877
|
+
});
|
|
5878
|
+
this._namedQueries = new NamedQueriesApi(
|
|
5879
|
+
this.transport,
|
|
5880
|
+
this.database,
|
|
5881
|
+
onNamedArtifactWarning
|
|
5882
|
+
);
|
|
5883
|
+
this._namedMutations = new NamedMutationsApi(
|
|
5884
|
+
this.transport,
|
|
5885
|
+
this.database,
|
|
5886
|
+
onNamedArtifactWarning
|
|
5887
|
+
);
|
|
5668
5888
|
}
|
|
5669
5889
|
/**
|
|
5670
5890
|
* Connects to the Aouda server.
|
|
@@ -5807,6 +6027,18 @@ var AoudaClient = class {
|
|
|
5807
6027
|
get materializedQueries() {
|
|
5808
6028
|
return this._materializedQueries;
|
|
5809
6029
|
}
|
|
6030
|
+
/**
|
|
6031
|
+
* Hash-only named-query execute and batch. Names are codegen aliases (D-5).
|
|
6032
|
+
*/
|
|
6033
|
+
get namedQueries() {
|
|
6034
|
+
return this._namedQueries;
|
|
6035
|
+
}
|
|
6036
|
+
/**
|
|
6037
|
+
* Hash-only named-mutation execute. No batch.
|
|
6038
|
+
*/
|
|
6039
|
+
get namedMutations() {
|
|
6040
|
+
return this._namedMutations;
|
|
6041
|
+
}
|
|
5810
6042
|
/**
|
|
5811
6043
|
* Access auth operations (signUp, signIn, signOut, refresh, me, changePassword).
|
|
5812
6044
|
* @returns The auth API.
|
|
@@ -5954,7 +6186,7 @@ Usage:
|
|
|
5954
6186
|
npx @aouda/client schema <command> [options]
|
|
5955
6187
|
|
|
5956
6188
|
Commands:
|
|
5957
|
-
generate Fetch schema from Aouda server and output TypeScript types
|
|
6189
|
+
generate Fetch schema from Aouda server and output TypeScript types (tables + named query/mutation hashes)
|
|
5958
6190
|
schema Schema management (diff, apply, export, validate, history, seed)
|
|
5959
6191
|
diff Show migration plan (desired vs current)
|
|
5960
6192
|
apply Apply schema (use --allow-destructive for drops; --dry-run to preview)
|