@lossless.org/nosqldb 10.11.0 → 11.0.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/.smartconfig.json +117 -0
- package/dist_rust/rustdb_linux_amd64 +0 -0
- package/dist_rust/rustdb_linux_amd64.tsrust-build.json +5 -5
- package/dist_rust/rustdb_linux_amd64_musl +0 -0
- package/dist_rust/rustdb_linux_amd64_musl.tsrust-build.json +5 -5
- package/dist_rust/rustdb_linux_arm64 +0 -0
- package/dist_rust/rustdb_linux_arm64.tsrust-build.json +5 -5
- package/dist_rust/rustdb_linux_arm64_musl +0 -0
- package/dist_rust/rustdb_linux_arm64_musl.tsrust-build.json +5 -5
- package/dist_rust/rustdb_macos_amd64 +0 -0
- package/dist_rust/rustdb_macos_amd64.tsrust-build.json +5 -5
- package/dist_rust/rustdb_macos_arm64 +0 -0
- package/dist_rust/rustdb_macos_arm64.tsrust-build.json +5 -5
- package/dist_ts/00_commitinfo_data.js +2 -2
- package/dist_ts/ts_local/classes.localnosqldb.d.ts +15 -4
- package/dist_ts/ts_local/classes.localnosqldb.js +31 -11
- package/dist_ts/ts_nosqldb/index.d.ts +3 -3
- package/dist_ts/ts_nosqldb/index.js +5 -4
- package/dist_ts/ts_nosqldb/rust-db-bridge.d.ts +125 -2
- package/dist_ts/ts_nosqldb/rust-db-bridge.js +261 -46
- package/dist_ts/ts_nosqldb/server/classes.nosqldbserver.d.ts +69 -3
- package/dist_ts/ts_nosqldb/server/classes.nosqldbserver.js +91 -12
- package/dist_ts/ts_nosqldb/server/index.d.ts +2 -2
- package/dist_ts/ts_nosqldb/server/index.js +2 -2
- package/dist_ts/ts_nosqldb/stopped-storage.d.ts +9 -1
- package/dist_ts/ts_nosqldb/stopped-storage.js +1 -1
- package/dist_ts/ts_nosqldb/storage-rehearsal.js +1 -1
- package/dist_ts/ts_nosqldb/storage-root-alignment.js +7 -3
- package/dist_ts/ts_nosqldb/storage-root-relocation.js +2 -2
- package/dist_ts_debugserver/bundled.js +6 -2
- package/package.json +28 -15
- package/readme.md +470 -730
- package/third-party-notices.md +40 -8
- package/third_party_licenses/inventory.json +6 -6
- package/ts/00_commitinfo_data.ts +8 -0
- package/ts/index.ts +85 -0
- package/ts/ts_local/classes.localnosqldb.ts +631 -0
- package/ts/ts_local/index.ts +19 -0
- package/ts/ts_local/plugins.ts +1 -0
- package/ts/ts_nosqldb/classes.storagerehearsal.ts +109 -0
- package/ts/ts_nosqldb/committed-writes.ts +38 -0
- package/ts/ts_nosqldb/file-storage-options.ts +33 -0
- package/ts/ts_nosqldb/file-storage-startup.ts +10 -0
- package/ts/ts_nosqldb/index.ts +132 -0
- package/ts/ts_nosqldb/offline-inspection.ts +375 -0
- package/ts/ts_nosqldb/plugins.ts +12 -0
- package/ts/ts_nosqldb/resource-fencing.ts +368 -0
- package/ts/ts_nosqldb/rust-db-bridge.ts +1309 -0
- package/ts/ts_nosqldb/server/classes.nosqldbserver.ts +874 -0
- package/ts/ts_nosqldb/server/index.ts +4 -0
- package/ts/ts_nosqldb/service-types.ts +499 -0
- package/ts/ts_nosqldb/stopped-storage.ts +116 -0
- package/ts/ts_nosqldb/storage-rehearsal.ts +215 -0
- package/ts/ts_nosqldb/storage-root-alignment.ts +180 -0
- package/ts/ts_nosqldb/storage-root-relocation.ts +279 -0
|
@@ -0,0 +1,874 @@
|
|
|
1
|
+
import {
|
|
2
|
+
RustDbBridge,
|
|
3
|
+
NoSqlDbEngineExitError,
|
|
4
|
+
NoSqlDbManagementOperationTerminationError,
|
|
5
|
+
attachNoSqlDbEngineStderrTail,
|
|
6
|
+
} from '../rust-db-bridge.js';
|
|
7
|
+
import { validateNoSqlDbManagementOperationOptions } from '../offline-inspection.js';
|
|
8
|
+
import type { INoSqlDbGetCommittedWritesInput, INoSqlDbCommittedWritesPage } from '../committed-writes.js';
|
|
9
|
+
import { validateNoSqlDbStorageRehearsalStartup } from '../storage-rehearsal.js';
|
|
10
|
+
import { assertNoLocalNoSqlDbStorageRootRelocationWorkspace } from '../storage-root-relocation.js';
|
|
11
|
+
import {
|
|
12
|
+
type TNoSqlDbFileStorageStartup,
|
|
13
|
+
} from '../file-storage-startup.js';
|
|
14
|
+
import {
|
|
15
|
+
normalizeNoSqlDbFileStorageOptions,
|
|
16
|
+
} from '../file-storage-options.js';
|
|
17
|
+
import type {
|
|
18
|
+
IOpLogEntry,
|
|
19
|
+
IOpLogResult,
|
|
20
|
+
IOpLogStats,
|
|
21
|
+
IRevertResult,
|
|
22
|
+
ICollectionInfo,
|
|
23
|
+
IDocumentsResult,
|
|
24
|
+
INoSqlDbMetrics,
|
|
25
|
+
INoSqlDbQueryOptions,
|
|
26
|
+
INoSqlDbInboundOptions,
|
|
27
|
+
} from '../rust-db-bridge.js';
|
|
28
|
+
import type {
|
|
29
|
+
INoSqlDbHealth,
|
|
30
|
+
INoSqlDbDatabaseTenantInput,
|
|
31
|
+
INoSqlDbAttestDatabaseTenantInput,
|
|
32
|
+
INoSqlDbAttestDatabaseTenantResult,
|
|
33
|
+
INoSqlDbEnsureDatabaseTenantInput,
|
|
34
|
+
INoSqlDbEnsureDatabaseTenantResult,
|
|
35
|
+
INoSqlDbAllocateDatabaseTenantInput,
|
|
36
|
+
INoSqlDbAllocateDatabaseTenantResult,
|
|
37
|
+
INoSqlDbDeleteDatabaseTenantInput,
|
|
38
|
+
INoSqlDbRotateDatabaseTenantPasswordInput,
|
|
39
|
+
INoSqlDbDatabaseTenantDescriptor,
|
|
40
|
+
INoSqlDbDeleteDatabaseTenantResult,
|
|
41
|
+
INoSqlDbIssueDatabaseAllocationReadAccessGrantInput,
|
|
42
|
+
INoSqlDbIssueDatabaseAllocationReadAccessGrantResult,
|
|
43
|
+
INoSqlDbRevokeDatabaseAllocationReadAccessGrantInput,
|
|
44
|
+
INoSqlDbRevokeDatabaseAllocationReadAccessGrantResult,
|
|
45
|
+
INoSqlDbDatabaseExport,
|
|
46
|
+
INoSqlDbExportDatabaseInput,
|
|
47
|
+
INoSqlDbExportHeldDatabaseInput,
|
|
48
|
+
INoSqlDbDatabaseContentDigest,
|
|
49
|
+
INoSqlDbGetDatabaseContentDigestInput,
|
|
50
|
+
INoSqlDbImportDatabaseInput,
|
|
51
|
+
INoSqlDbImportDatabaseResult,
|
|
52
|
+
INoSqlDbCommitDatabasePublicationInput,
|
|
53
|
+
INoSqlDbManagementOperationOptions,
|
|
54
|
+
INoSqlDbDatabaseResourceFenceState,
|
|
55
|
+
TNoSqlDbCommitDatabasePublicationResult,
|
|
56
|
+
} from '../service-types.js';
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Server configuration options
|
|
60
|
+
*/
|
|
61
|
+
export interface INoSqlDbServerOptions {
|
|
62
|
+
/** Exact engine executable. Invalid explicit paths reject without chmod or discovery fallback. Omit for normal discovery. */
|
|
63
|
+
binaryPath?: string;
|
|
64
|
+
/** Exact rehearsal operation ID. Requires authenticated current-format-only file storage and an explicit private Unix socket. Never promotes a rehearsal. */
|
|
65
|
+
storageRehearsalId?: string;
|
|
66
|
+
/** Default: current-format-only. existing-only also requires initialized storage and refuses fresh creation. */
|
|
67
|
+
fileStorageStartup?: TNoSqlDbFileStorageStartup;
|
|
68
|
+
/** Port to listen on (default: 27017, 0 requests an OS-assigned port) - ignored if socketPath is set */
|
|
69
|
+
port?: number;
|
|
70
|
+
/** Host to bind to (default: 127.0.0.1) - ignored if socketPath is set */
|
|
71
|
+
host?: string;
|
|
72
|
+
/** Unix socket path - if set, server listens on socket instead of TCP */
|
|
73
|
+
socketPath?: string;
|
|
74
|
+
/** Storage type: 'memory' or 'file' (default: 'memory') */
|
|
75
|
+
storage?: 'memory' | 'file';
|
|
76
|
+
/** Path for file storage (required if storage is 'file') */
|
|
77
|
+
storagePath?: string;
|
|
78
|
+
/** Authentication configuration. Disabled by default. */
|
|
79
|
+
auth?: INoSqlDbAuthOptions;
|
|
80
|
+
/** TLS transport configuration for TCP listeners. Disabled by default. */
|
|
81
|
+
tls?: INoSqlDbTlsOptions;
|
|
82
|
+
/** In-memory oplog: whether it is kept, and its retention limits (defaults: enabled, 10000 entries / 8 MiB). */
|
|
83
|
+
oplog?: INoSqlDbOplogOptions;
|
|
84
|
+
/** Memory, temporary storage, and concurrency admission for large sorts. */
|
|
85
|
+
query?: INoSqlDbQueryOptions;
|
|
86
|
+
/** Memory budgets for received requests (default: sized from the memory limit). */
|
|
87
|
+
inbound?: INoSqlDbInboundOptions;
|
|
88
|
+
/**
|
|
89
|
+
* The store's memory budget in bytes, which its node cache and write buffer
|
|
90
|
+
* share: 4 MiB to 1 TiB. The engine refuses a start with a value outside
|
|
91
|
+
* that range and never clamps it. Unset, the budget is computed from the
|
|
92
|
+
* memory the process may use.
|
|
93
|
+
*/
|
|
94
|
+
storageBudgetBytes?: number;
|
|
95
|
+
/**
|
|
96
|
+
* Receives every line the engine writes to its standard error, in order and
|
|
97
|
+
* without its line break. Without it, nothing is written to the host
|
|
98
|
+
* process's standard error. Start failures and unexpected engine exits
|
|
99
|
+
* carry the engine's last lines either way, in `engineStderrTail`. Each
|
|
100
|
+
* call runs in its own microtask; an exception it throws is not caught and
|
|
101
|
+
* reaches the process as an uncaught exception.
|
|
102
|
+
*/
|
|
103
|
+
engineLog?: (line: string) => void;
|
|
104
|
+
/**
|
|
105
|
+
* Called once for each unexpected engine exit: an exit with a non-zero
|
|
106
|
+
* status or by a signal while the server runs and no stop is in progress.
|
|
107
|
+
* An exit with status 0, which the engine makes after a graceful shutdown
|
|
108
|
+
* it was signalled to make, is not reported; the server counts as stopped
|
|
109
|
+
* either way. The call runs in a microtask after the server has recorded the
|
|
110
|
+
* exit. An exception it throws is not caught and reaches the process as an
|
|
111
|
+
* uncaught exception.
|
|
112
|
+
*/
|
|
113
|
+
onEngineExit?: (error: NoSqlDbEngineExitError) => void;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface INoSqlDbOplogOptions {
|
|
117
|
+
/** Keep the oplog (default: true). When false, writes prepare no history, and getOpLog, getOpLogStats and revertToSeq reject with "oplog is disabled". */
|
|
118
|
+
enabled?: boolean;
|
|
119
|
+
/** Maximum number of retained oplog entries (default: 10000). */
|
|
120
|
+
maxEntries?: number;
|
|
121
|
+
/** Maximum approximate retained bytes, the entry table included (default: 8 MiB). Oldest entries are evicted first; reverts cannot reach past evicted entries. */
|
|
122
|
+
maxBytes?: number;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface INoSqlDbAuthOptions {
|
|
126
|
+
enabled?: boolean;
|
|
127
|
+
users?: INoSqlDbAuthUser[];
|
|
128
|
+
scramIterations?: number;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface INoSqlDbAuthUser {
|
|
132
|
+
username: string;
|
|
133
|
+
password: string;
|
|
134
|
+
database?: string;
|
|
135
|
+
roles?: string[];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface INoSqlDbTlsOptions {
|
|
139
|
+
enabled?: boolean;
|
|
140
|
+
certPath?: string;
|
|
141
|
+
keyPath?: string;
|
|
142
|
+
caPath?: string;
|
|
143
|
+
requireClientCert?: boolean;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* How `stop()` ended the engine. A stop is clean exactly when a stop request
|
|
148
|
+
* was sent and acknowledged, the server neither signalled nor killed the
|
|
149
|
+
* engine, and the engine exited with code 0 and no signal; `clean` says so.
|
|
150
|
+
*/
|
|
151
|
+
export interface INoSqlDbServerStopResult {
|
|
152
|
+
/** Whether a stop request was sent to a running engine. */
|
|
153
|
+
stopRequestSent: boolean;
|
|
154
|
+
/** Whether the engine acknowledged that request. */
|
|
155
|
+
stopAcknowledged: boolean;
|
|
156
|
+
/**
|
|
157
|
+
* Whether the server had to signal or kill the engine to end it: the engine
|
|
158
|
+
* had not exited by itself within `noSqlDbServerStopGraceMs` of an
|
|
159
|
+
* acknowledged request, or no request was acknowledged while it ran.
|
|
160
|
+
*/
|
|
161
|
+
forcedByServer: boolean;
|
|
162
|
+
/** The engine's exit code as its process exit reported it, or null. */
|
|
163
|
+
exitCode: number | null;
|
|
164
|
+
/** The signal that ended the engine as its process exit reported it, or null. */
|
|
165
|
+
exitSignal: string | null;
|
|
166
|
+
/** Whether the stop was clean, as defined above. */
|
|
167
|
+
clean: boolean;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* How long `stop()` waits, after the engine has acknowledged the stop request
|
|
172
|
+
* and its management input is closed, for the engine to exit by itself before
|
|
173
|
+
* the server ends it.
|
|
174
|
+
*/
|
|
175
|
+
export const noSqlDbServerStopGraceMs = 10_000;
|
|
176
|
+
|
|
177
|
+
/** One stop's result, frozen, as all of its callers share it. */
|
|
178
|
+
const stopResult = (
|
|
179
|
+
stopRequestSent: boolean,
|
|
180
|
+
stopAcknowledged: boolean,
|
|
181
|
+
forcedByServer: boolean,
|
|
182
|
+
exit: { code: number | null; signal: string | null } | null,
|
|
183
|
+
): INoSqlDbServerStopResult => {
|
|
184
|
+
const exitCode = exit?.code ?? null;
|
|
185
|
+
const exitSignal = exit?.signal ?? null;
|
|
186
|
+
return Object.freeze({
|
|
187
|
+
stopRequestSent,
|
|
188
|
+
stopAcknowledged,
|
|
189
|
+
forcedByServer,
|
|
190
|
+
exitCode,
|
|
191
|
+
exitSignal,
|
|
192
|
+
clean: stopRequestSent && stopAcknowledged && !forcedByServer && exitCode === 0 && exitSignal === null,
|
|
193
|
+
});
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
/** The result of a stop when no engine runs: no request was sent. */
|
|
197
|
+
export const noStopRequestResult = stopResult(false, false, false, null);
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* NoSqlDbServer - Wire protocol compatible database server backed by Rust
|
|
201
|
+
*
|
|
202
|
+
* This server implements the wire protocol to allow official drivers to
|
|
203
|
+
* connect and perform operations. The core engine runs as a Rust sidecar
|
|
204
|
+
* binary managed via @push.rocks/smartrust IPC.
|
|
205
|
+
*
|
|
206
|
+
* @example
|
|
207
|
+
* ```typescript
|
|
208
|
+
* import { NoSqlDbServer } from '@lossless.org/nosqldb';
|
|
209
|
+
* import { MongoClient } from 'mongodb';
|
|
210
|
+
*
|
|
211
|
+
* const server = new NoSqlDbServer({ port: 27017 });
|
|
212
|
+
* await server.start();
|
|
213
|
+
*
|
|
214
|
+
* const client = new MongoClient(server.getConnectionUri());
|
|
215
|
+
* await client.connect();
|
|
216
|
+
* ```
|
|
217
|
+
*/
|
|
218
|
+
export class NoSqlDbServer {
|
|
219
|
+
private options: INoSqlDbServerOptions;
|
|
220
|
+
private bridge: RustDbBridge;
|
|
221
|
+
private bridgeOwned = false;
|
|
222
|
+
private startInProgress = false;
|
|
223
|
+
private startCompletionPromise: Promise<void> | null = null;
|
|
224
|
+
private stopCompletionPromise: Promise<INoSqlDbServerStopResult> | null = null;
|
|
225
|
+
private isRunning = false;
|
|
226
|
+
private resolvedConnectionUri = '';
|
|
227
|
+
private resolvedPort: number | undefined;
|
|
228
|
+
|
|
229
|
+
constructor(options: INoSqlDbServerOptions = {}) {
|
|
230
|
+
validateNoSqlDbStorageRehearsalStartup(options);
|
|
231
|
+
const fileStorage = normalizeNoSqlDbFileStorageOptions(options);
|
|
232
|
+
this.options = {
|
|
233
|
+
binaryPath: options.binaryPath,
|
|
234
|
+
storageRehearsalId: options.storageRehearsalId,
|
|
235
|
+
...fileStorage,
|
|
236
|
+
port: options.port ?? 27017,
|
|
237
|
+
host: options.host ?? '127.0.0.1',
|
|
238
|
+
socketPath: options.socketPath,
|
|
239
|
+
storage: options.storage ?? 'memory',
|
|
240
|
+
storagePath: options.storagePath ?? './data',
|
|
241
|
+
auth: options.auth,
|
|
242
|
+
tls: options.tls,
|
|
243
|
+
oplog: options.oplog,
|
|
244
|
+
query: options.query,
|
|
245
|
+
inbound: options.inbound,
|
|
246
|
+
storageBudgetBytes: options.storageBudgetBytes,
|
|
247
|
+
engineLog: options.engineLog,
|
|
248
|
+
onEngineExit: options.onEngineExit,
|
|
249
|
+
};
|
|
250
|
+
this.bridge = new RustDbBridge({ binaryPath: this.options.binaryPath, engineLog: this.options.engineLog });
|
|
251
|
+
this.bridge.on('exit', (code: number | null, signal: string | null) => {
|
|
252
|
+
// A stop keeps the running state until it has confirmed the bridge's
|
|
253
|
+
// termination, also when the engine exits by itself first.
|
|
254
|
+
if (this.stopCompletionPromise) return;
|
|
255
|
+
const exitedUnexpectedly = this.isRunning
|
|
256
|
+
&& (code !== 0 || signal !== null);
|
|
257
|
+
this.isRunning = false;
|
|
258
|
+
this.resolvedConnectionUri = '';
|
|
259
|
+
this.resolvedPort = undefined;
|
|
260
|
+
if (exitedUnexpectedly) {
|
|
261
|
+
const onEngineExit = this.options.onEngineExit;
|
|
262
|
+
const exit = new NoSqlDbEngineExitError(code, signal, this.bridge.engineStderrTail);
|
|
263
|
+
// Outside the bridge's exit event, so a throwing callback cannot
|
|
264
|
+
// interrupt the bridge's own exit handling.
|
|
265
|
+
if (onEngineExit) queueMicrotask(() => onEngineExit(exit));
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Start the server. A rejection carries `engineStderrTail`: the last
|
|
272
|
+
* standard-error lines of the engine this start spawned, or none.
|
|
273
|
+
*/
|
|
274
|
+
async start(optionsArg?: INoSqlDbManagementOperationOptions): Promise<void> {
|
|
275
|
+
const attempt = { engineSpawned: false };
|
|
276
|
+
try {
|
|
277
|
+
await this.startAttempt(optionsArg, attempt);
|
|
278
|
+
} catch (error) {
|
|
279
|
+
// Read after the attempt's cleanup termination, once the engine's last
|
|
280
|
+
// lines have arrived.
|
|
281
|
+
attachNoSqlDbEngineStderrTail(error, attempt.engineSpawned ? this.bridge.engineStderrTail : []);
|
|
282
|
+
throw error;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private async startAttempt(
|
|
287
|
+
optionsArg: INoSqlDbManagementOperationOptions | undefined,
|
|
288
|
+
attempt: { engineSpawned: boolean },
|
|
289
|
+
): Promise<void> {
|
|
290
|
+
validateNoSqlDbManagementOperationOptions(optionsArg);
|
|
291
|
+
optionsArg?.signal?.throwIfAborted();
|
|
292
|
+
if (this.isRunning || this.startInProgress || this.bridgeOwned || this.stopCompletionPromise) {
|
|
293
|
+
throw new Error('Server is already running or still owns a Rust bridge');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const startupStartedAt = Date.now();
|
|
297
|
+
const startupDeadline = optionsArg?.timeoutMs === undefined
|
|
298
|
+
? undefined
|
|
299
|
+
: startupStartedAt + optionsArg.timeoutMs;
|
|
300
|
+
const timeoutError = new DOMException(
|
|
301
|
+
optionsArg?.timeoutMs === undefined
|
|
302
|
+
? 'NoSQLDB startup timed out'
|
|
303
|
+
: `NoSQLDB startup timed out after ${optionsArg.timeoutMs}ms`,
|
|
304
|
+
'TimeoutError',
|
|
305
|
+
);
|
|
306
|
+
const startupAbortController = new AbortController();
|
|
307
|
+
let startupTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
308
|
+
let externalAbortHandler: (() => void) | undefined;
|
|
309
|
+
if (optionsArg?.signal) {
|
|
310
|
+
externalAbortHandler = () => {
|
|
311
|
+
startupAbortController.abort(optionsArg.signal!.reason);
|
|
312
|
+
};
|
|
313
|
+
optionsArg.signal.addEventListener('abort', externalAbortHandler, { once: true });
|
|
314
|
+
}
|
|
315
|
+
if (optionsArg?.timeoutMs !== undefined) {
|
|
316
|
+
startupTimeout = setTimeout(() => {
|
|
317
|
+
startupAbortController.abort(timeoutError);
|
|
318
|
+
}, optionsArg.timeoutMs);
|
|
319
|
+
}
|
|
320
|
+
const checkpoint = () => {
|
|
321
|
+
if (
|
|
322
|
+
startupDeadline !== undefined
|
|
323
|
+
&& Date.now() >= startupDeadline
|
|
324
|
+
&& !startupAbortController.signal.aborted
|
|
325
|
+
) {
|
|
326
|
+
startupAbortController.abort(timeoutError);
|
|
327
|
+
}
|
|
328
|
+
startupAbortController.signal.throwIfAborted();
|
|
329
|
+
};
|
|
330
|
+
const getBridgeOperationOptions = (): INoSqlDbManagementOperationOptions | undefined => {
|
|
331
|
+
checkpoint();
|
|
332
|
+
const remainingTimeoutMs = startupDeadline === undefined
|
|
333
|
+
? undefined
|
|
334
|
+
: startupDeadline - Date.now();
|
|
335
|
+
if (remainingTimeoutMs !== undefined && remainingTimeoutMs <= 0) {
|
|
336
|
+
startupAbortController.abort(timeoutError);
|
|
337
|
+
checkpoint();
|
|
338
|
+
}
|
|
339
|
+
if (!optionsArg?.signal && remainingTimeoutMs === undefined) return undefined;
|
|
340
|
+
return {
|
|
341
|
+
...(optionsArg?.signal ? { signal: optionsArg.signal } : {}),
|
|
342
|
+
...(remainingTimeoutMs === undefined ? {} : { timeoutMs: remainingTimeoutMs }),
|
|
343
|
+
};
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
this.startInProgress = true;
|
|
347
|
+
let resolveStartCompletion!: () => void;
|
|
348
|
+
const startCompletionPromise = new Promise<void>((resolveArg) => {
|
|
349
|
+
resolveStartCompletion = resolveArg;
|
|
350
|
+
});
|
|
351
|
+
this.startCompletionPromise = startCompletionPromise;
|
|
352
|
+
try {
|
|
353
|
+
checkpoint();
|
|
354
|
+
// Relocation workspace admission is read-only; the native owner alone
|
|
355
|
+
// classifies and initializes storage after acquiring its root lease.
|
|
356
|
+
if (this.options.storage === 'file' && this.options.storagePath) {
|
|
357
|
+
await assertNoLocalNoSqlDbStorageRootRelocationWorkspace(this.options.storagePath);
|
|
358
|
+
checkpoint();
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Ownership starts before spawn because a rejected spawn can still have
|
|
362
|
+
// created a child whose termination must be confirmed.
|
|
363
|
+
this.bridgeOwned = true;
|
|
364
|
+
const spawnOptions = getBridgeOperationOptions();
|
|
365
|
+
attempt.engineSpawned = true;
|
|
366
|
+
const spawned = await this.bridge.spawn(spawnOptions);
|
|
367
|
+
checkpoint();
|
|
368
|
+
if (!spawned) {
|
|
369
|
+
// The bridge states what it knows: that the engine at its binaryPath did not report ready, or, without one, both meanings.
|
|
370
|
+
throw new Error(this.bridge.spawnFailureMessage ?? 'the nosqldb engine did not start: spawn() returned false');
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Send config, get back connectionUri
|
|
374
|
+
const result = await this.bridge.startDb({
|
|
375
|
+
port: this.options.port,
|
|
376
|
+
host: this.options.host,
|
|
377
|
+
socketPath: this.options.socketPath,
|
|
378
|
+
storage: this.options.storage ?? 'memory',
|
|
379
|
+
storagePath: this.options.storagePath,
|
|
380
|
+
...(this.options.fileStorageStartup === 'existing-only' ? { fileStorageStartup: 'existing-only' as const } : {}),
|
|
381
|
+
storageRehearsalId: this.options.storageRehearsalId,
|
|
382
|
+
auth: this.options.auth,
|
|
383
|
+
tls: this.options.tls,
|
|
384
|
+
oplog: this.options.oplog,
|
|
385
|
+
query: this.options.query,
|
|
386
|
+
inbound: this.options.inbound,
|
|
387
|
+
storageBudgetBytes: this.options.storageBudgetBytes,
|
|
388
|
+
}, getBridgeOperationOptions());
|
|
389
|
+
checkpoint();
|
|
390
|
+
if (!this.bridgeOwned || !this.bridge.running) {
|
|
391
|
+
throw new Error('nosqldb Rust process exited during startup');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (!this.options.socketPath) {
|
|
395
|
+
const connectionUrl = new URL(result.connectionUri);
|
|
396
|
+
const resolvedPort = Number(connectionUrl.port);
|
|
397
|
+
if (
|
|
398
|
+
connectionUrl.protocol !== 'mongodb:'
|
|
399
|
+
|| !connectionUrl.hostname
|
|
400
|
+
|| !Number.isInteger(resolvedPort)
|
|
401
|
+
|| resolvedPort < 1
|
|
402
|
+
|| resolvedPort > 65_535
|
|
403
|
+
) {
|
|
404
|
+
throw new Error(`nosqldb returned an invalid TCP connection URI: ${result.connectionUri}`);
|
|
405
|
+
}
|
|
406
|
+
this.resolvedPort = resolvedPort;
|
|
407
|
+
}
|
|
408
|
+
this.resolvedConnectionUri = result.connectionUri;
|
|
409
|
+
checkpoint();
|
|
410
|
+
this.isRunning = true;
|
|
411
|
+
} catch (error) {
|
|
412
|
+
const startupError = error instanceof NoSqlDbManagementOperationTerminationError
|
|
413
|
+
? error
|
|
414
|
+
: optionsArg?.signal?.aborted
|
|
415
|
+
? optionsArg.signal.reason
|
|
416
|
+
: startupAbortController.signal.aborted || (
|
|
417
|
+
startupDeadline !== undefined && Date.now() >= startupDeadline
|
|
418
|
+
)
|
|
419
|
+
? timeoutError
|
|
420
|
+
: error;
|
|
421
|
+
this.isRunning = false;
|
|
422
|
+
this.resolvedConnectionUri = '';
|
|
423
|
+
this.resolvedPort = undefined;
|
|
424
|
+
if (this.bridgeOwned) {
|
|
425
|
+
try {
|
|
426
|
+
await this.bridge.terminate();
|
|
427
|
+
this.bridgeOwned = false;
|
|
428
|
+
} catch (cleanupError) {
|
|
429
|
+
throw new AggregateError(
|
|
430
|
+
[startupError, cleanupError],
|
|
431
|
+
'NoSQLDB startup failed and Rust bridge cleanup was incomplete',
|
|
432
|
+
{ cause: startupError },
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
throw startupError;
|
|
437
|
+
} finally {
|
|
438
|
+
if (startupTimeout) clearTimeout(startupTimeout);
|
|
439
|
+
if (externalAbortHandler && optionsArg?.signal) {
|
|
440
|
+
optionsArg.signal.removeEventListener('abort', externalAbortHandler);
|
|
441
|
+
}
|
|
442
|
+
this.startInProgress = false;
|
|
443
|
+
resolveStartCompletion();
|
|
444
|
+
if (this.startCompletionPromise === startCompletionPromise) {
|
|
445
|
+
this.startCompletionPromise = null;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Stop the server: wait for active startup work, send the engine a stop
|
|
452
|
+
* request, close its management input once it has acknowledged, give it
|
|
453
|
+
* `noSqlDbServerStopGraceMs` to exit by itself, and end it otherwise; then
|
|
454
|
+
* confirm the bridge's cleanup. Resolves with how the engine ended, which
|
|
455
|
+
* concurrent callers share; when no engine runs, the result says that no
|
|
456
|
+
* request was sent. Rejects, retaining ownership for a retry, only when the
|
|
457
|
+
* bridge's termination fails.
|
|
458
|
+
*/
|
|
459
|
+
stop(): Promise<INoSqlDbServerStopResult> {
|
|
460
|
+
if (this.stopCompletionPromise) {
|
|
461
|
+
return this.stopCompletionPromise;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const stopCompletionPromise = this.stopInternal();
|
|
465
|
+
this.stopCompletionPromise = stopCompletionPromise;
|
|
466
|
+
const releaseStopCompletion = () => {
|
|
467
|
+
if (this.stopCompletionPromise === stopCompletionPromise) {
|
|
468
|
+
this.stopCompletionPromise = null;
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
void stopCompletionPromise.then(releaseStopCompletion, releaseStopCompletion);
|
|
472
|
+
return stopCompletionPromise;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
private async stopInternal(): Promise<INoSqlDbServerStopResult> {
|
|
476
|
+
const startCompletionPromise = this.startCompletionPromise;
|
|
477
|
+
if (startCompletionPromise) {
|
|
478
|
+
await startCompletionPromise;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (!this.isRunning && !this.bridgeOwned) {
|
|
482
|
+
return noStopRequestResult;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const requestGracefulStop = this.isRunning && this.bridge.running;
|
|
486
|
+
let stopAcknowledged = false;
|
|
487
|
+
if (requestGracefulStop) {
|
|
488
|
+
try {
|
|
489
|
+
await this.bridge.stopDb();
|
|
490
|
+
stopAcknowledged = true;
|
|
491
|
+
} catch {
|
|
492
|
+
// Confirmed bridge termination below still releases the owned process.
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
// An engine that acknowledged the stop exits by itself once its management
|
|
496
|
+
// input closes; it is ended only if it has not within the grace.
|
|
497
|
+
if (stopAcknowledged && this.bridge.engineProcessAlive) {
|
|
498
|
+
this.bridge.endManagementInput();
|
|
499
|
+
await this.bridge.waitForEngineExit(noSqlDbServerStopGraceMs);
|
|
500
|
+
}
|
|
501
|
+
const forcedByServer = this.bridge.engineProcessAlive;
|
|
502
|
+
|
|
503
|
+
try {
|
|
504
|
+
await this.bridge.terminate();
|
|
505
|
+
} catch (error) {
|
|
506
|
+
// An engine that has exited no longer runs, as the exit handler would
|
|
507
|
+
// have recorded outside a stop; ownership stays for a retry.
|
|
508
|
+
if (this.bridge.lastEngineExit !== null) {
|
|
509
|
+
this.isRunning = false;
|
|
510
|
+
this.resolvedConnectionUri = '';
|
|
511
|
+
this.resolvedPort = undefined;
|
|
512
|
+
}
|
|
513
|
+
throw error;
|
|
514
|
+
}
|
|
515
|
+
this.bridgeOwned = false;
|
|
516
|
+
this.isRunning = false;
|
|
517
|
+
this.resolvedConnectionUri = '';
|
|
518
|
+
this.resolvedPort = undefined;
|
|
519
|
+
// The process's exit event may follow the confirmed termination by a tick.
|
|
520
|
+
if (this.bridge.engineProcessAlive) await this.bridge.waitForEngineExit(1_000);
|
|
521
|
+
return stopResult(requestGracefulStop, stopAcknowledged, forcedByServer, this.bridge.lastEngineExit);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Get the connection URI for this server
|
|
526
|
+
*/
|
|
527
|
+
getConnectionUri(): string {
|
|
528
|
+
if (this.resolvedConnectionUri) {
|
|
529
|
+
return this.resolvedConnectionUri;
|
|
530
|
+
}
|
|
531
|
+
// Fallback: compute from options
|
|
532
|
+
if (this.options.socketPath) {
|
|
533
|
+
const encodedPath = encodeURIComponent(this.options.socketPath);
|
|
534
|
+
return `mongodb://${encodedPath}`;
|
|
535
|
+
}
|
|
536
|
+
const baseUri = `mongodb://${this.options.host ?? '127.0.0.1'}:${this.options.port ?? 27017}`;
|
|
537
|
+
return this.options.tls?.enabled ? `${baseUri}/?tls=true` : baseUri;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Get the socket path (if using Unix socket mode)
|
|
542
|
+
*/
|
|
543
|
+
get socketPath(): string | undefined {
|
|
544
|
+
return this.options.socketPath;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Check if the server is running
|
|
549
|
+
*/
|
|
550
|
+
/** Process id of the spawned Rust engine, when it runs as a child process. */
|
|
551
|
+
get processId(): number | undefined {
|
|
552
|
+
return this.bridge.processId;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
get running(): boolean {
|
|
556
|
+
return this.isRunning;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
private async runDatabaseManagementOperation<TResult>(
|
|
560
|
+
operationArg: () => Promise<TResult>,
|
|
561
|
+
): Promise<TResult> {
|
|
562
|
+
try {
|
|
563
|
+
return await operationArg();
|
|
564
|
+
} catch (error) {
|
|
565
|
+
if (
|
|
566
|
+
!this.bridge.running
|
|
567
|
+
|| error instanceof NoSqlDbManagementOperationTerminationError
|
|
568
|
+
) {
|
|
569
|
+
this.isRunning = false;
|
|
570
|
+
this.resolvedConnectionUri = '';
|
|
571
|
+
this.resolvedPort = undefined;
|
|
572
|
+
if (this.bridge.ownershipReleased) {
|
|
573
|
+
this.bridgeOwned = false;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
throw error;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Get the port the server is listening on
|
|
582
|
+
*/
|
|
583
|
+
get port(): number {
|
|
584
|
+
return this.resolvedPort ?? this.options.port ?? 27017;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Get the host the server is bound to
|
|
589
|
+
*/
|
|
590
|
+
get host(): string {
|
|
591
|
+
return this.options.host ?? '127.0.0.1';
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Create an isolated database/user pair for an application tenant.
|
|
596
|
+
*/
|
|
597
|
+
async createDatabaseTenant(
|
|
598
|
+
params: INoSqlDbDatabaseTenantInput,
|
|
599
|
+
): Promise<INoSqlDbDatabaseTenantDescriptor> {
|
|
600
|
+
const descriptor = await this.bridge.createDatabaseTenant(params);
|
|
601
|
+
return this.withTenantMongoUri(descriptor, params.password);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/** Authoritatively allocate an absent, fenced database tenant. */
|
|
605
|
+
async allocateDatabaseTenant(
|
|
606
|
+
params: INoSqlDbAllocateDatabaseTenantInput,
|
|
607
|
+
optionsArg?: INoSqlDbManagementOperationOptions,
|
|
608
|
+
): Promise<INoSqlDbAllocateDatabaseTenantResult> {
|
|
609
|
+
const descriptor = await this.runDatabaseManagementOperation(
|
|
610
|
+
() => this.bridge.allocateDatabaseTenant(params, optionsArg),
|
|
611
|
+
);
|
|
612
|
+
return this.withTenantMongoUri(descriptor, params.password);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Idempotently provision or rotate one exclusively owned database tenant.
|
|
617
|
+
*/
|
|
618
|
+
async ensureDatabaseTenant(
|
|
619
|
+
params: INoSqlDbEnsureDatabaseTenantInput,
|
|
620
|
+
optionsArg?: INoSqlDbManagementOperationOptions,
|
|
621
|
+
): Promise<INoSqlDbEnsureDatabaseTenantResult> {
|
|
622
|
+
const descriptor = await this.runDatabaseManagementOperation(
|
|
623
|
+
() => this.bridge.ensureDatabaseTenant(params, optionsArg),
|
|
624
|
+
);
|
|
625
|
+
return this.withTenantMongoUri(descriptor, params.password);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/** Verify an exact tenant credential without changing provider state. */
|
|
629
|
+
async attestDatabaseTenant(
|
|
630
|
+
params: INoSqlDbAttestDatabaseTenantInput,
|
|
631
|
+
optionsArg?: INoSqlDbManagementOperationOptions,
|
|
632
|
+
): Promise<INoSqlDbAttestDatabaseTenantResult> {
|
|
633
|
+
return this.runDatabaseManagementOperation(
|
|
634
|
+
() => this.bridge.attestDatabaseTenant(params, optionsArg),
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Delete a tenant database and its tenant user(s).
|
|
640
|
+
*/
|
|
641
|
+
async deleteDatabaseTenant(
|
|
642
|
+
params: INoSqlDbDeleteDatabaseTenantInput,
|
|
643
|
+
optionsArg?: INoSqlDbManagementOperationOptions,
|
|
644
|
+
): Promise<INoSqlDbDeleteDatabaseTenantResult> {
|
|
645
|
+
return this.runDatabaseManagementOperation(
|
|
646
|
+
() => this.bridge.deleteDatabaseTenant(params, optionsArg),
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/** Issue one fixed-expiry read grant bound to an exact active allocation. */
|
|
651
|
+
async issueDatabaseAllocationReadAccessGrant(
|
|
652
|
+
params: INoSqlDbIssueDatabaseAllocationReadAccessGrantInput,
|
|
653
|
+
optionsArg?: INoSqlDbManagementOperationOptions,
|
|
654
|
+
): Promise<INoSqlDbIssueDatabaseAllocationReadAccessGrantResult> {
|
|
655
|
+
const descriptor = await this.runDatabaseManagementOperation(
|
|
656
|
+
() => this.bridge.issueDatabaseAllocationReadAccessGrant(params, optionsArg),
|
|
657
|
+
);
|
|
658
|
+
return this.withTenantMongoUri(descriptor, params.password);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/** Idempotently revoke one allocation read grant without returning a secret. */
|
|
662
|
+
async revokeDatabaseAllocationReadAccessGrant(
|
|
663
|
+
params: INoSqlDbRevokeDatabaseAllocationReadAccessGrantInput,
|
|
664
|
+
optionsArg?: INoSqlDbManagementOperationOptions,
|
|
665
|
+
): Promise<INoSqlDbRevokeDatabaseAllocationReadAccessGrantResult> {
|
|
666
|
+
return this.runDatabaseManagementOperation(
|
|
667
|
+
() => this.bridge.revokeDatabaseAllocationReadAccessGrant(params, optionsArg),
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Rotate a tenant user's password without restarting the server.
|
|
673
|
+
*/
|
|
674
|
+
async rotateDatabaseTenantPassword(
|
|
675
|
+
params: INoSqlDbRotateDatabaseTenantPasswordInput,
|
|
676
|
+
): Promise<INoSqlDbDatabaseTenantDescriptor> {
|
|
677
|
+
const descriptor = await this.bridge.rotateDatabaseTenantPassword(params);
|
|
678
|
+
return this.withTenantMongoUri(descriptor, params.password);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* List known database tenants.
|
|
683
|
+
*/
|
|
684
|
+
async listDatabaseTenants(): Promise<INoSqlDbDatabaseTenantDescriptor[]> {
|
|
685
|
+
return this.bridge.listDatabaseTenants();
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Get a tenant descriptor without exposing a password.
|
|
690
|
+
*/
|
|
691
|
+
async getDatabaseTenantDescriptor(params: {
|
|
692
|
+
databaseName: string;
|
|
693
|
+
username: string;
|
|
694
|
+
}): Promise<INoSqlDbDatabaseTenantDescriptor> {
|
|
695
|
+
return this.bridge.getDatabaseTenantDescriptor(params);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/** Inspect the durable high-water mark without exposing mutation receipts. */
|
|
699
|
+
async getDatabaseResourceFenceState(
|
|
700
|
+
params: { databaseName: string },
|
|
701
|
+
optionsArg?: INoSqlDbManagementOperationOptions,
|
|
702
|
+
): Promise<INoSqlDbDatabaseResourceFenceState | null> {
|
|
703
|
+
return this.runDatabaseManagementOperation(
|
|
704
|
+
() => this.bridge.getDatabaseResourceFenceState(params, optionsArg),
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Export one database as an Extended JSON snapshot.
|
|
710
|
+
*/
|
|
711
|
+
async exportDatabase(
|
|
712
|
+
params: INoSqlDbExportDatabaseInput,
|
|
713
|
+
optionsArg?: INoSqlDbManagementOperationOptions,
|
|
714
|
+
): Promise<INoSqlDbDatabaseExport> {
|
|
715
|
+
return this.runDatabaseManagementOperation(
|
|
716
|
+
() => this.bridge.exportDatabase(params, optionsArg),
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/** Read an exact held publication without opening its serving data plane. */
|
|
721
|
+
async exportHeldDatabase(
|
|
722
|
+
params: INoSqlDbExportHeldDatabaseInput,
|
|
723
|
+
optionsArg?: INoSqlDbManagementOperationOptions,
|
|
724
|
+
): Promise<INoSqlDbDatabaseExport> {
|
|
725
|
+
return this.runDatabaseManagementOperation(
|
|
726
|
+
() => this.bridge.exportHeldDatabase(params, optionsArg),
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/** Compute a bounded, database-name-independent digest of logical database content. */
|
|
731
|
+
async getDatabaseContentDigest(
|
|
732
|
+
params: INoSqlDbGetDatabaseContentDigestInput,
|
|
733
|
+
optionsArg?: INoSqlDbManagementOperationOptions,
|
|
734
|
+
): Promise<INoSqlDbDatabaseContentDigest> {
|
|
735
|
+
return this.runDatabaseManagementOperation(
|
|
736
|
+
() => this.bridge.getDatabaseContentDigest(params, optionsArg),
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Replace one database with a previously exported snapshot.
|
|
742
|
+
*/
|
|
743
|
+
async importDatabase(
|
|
744
|
+
params: INoSqlDbImportDatabaseInput,
|
|
745
|
+
optionsArg?: INoSqlDbManagementOperationOptions,
|
|
746
|
+
): Promise<INoSqlDbImportDatabaseResult> {
|
|
747
|
+
return this.runDatabaseManagementOperation(
|
|
748
|
+
() => this.bridge.importDatabase(params, optionsArg),
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/** Release an exact held database publication after downstream fsync. */
|
|
753
|
+
async commitDatabasePublication(
|
|
754
|
+
params: INoSqlDbCommitDatabasePublicationInput,
|
|
755
|
+
optionsArg?: INoSqlDbManagementOperationOptions,
|
|
756
|
+
): Promise<TNoSqlDbCommitDatabasePublicationResult> {
|
|
757
|
+
return this.runDatabaseManagementOperation(
|
|
758
|
+
() => this.bridge.commitDatabasePublication(params, optionsArg),
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Get readiness/health details for long-running service use.
|
|
764
|
+
*/
|
|
765
|
+
async getHealth(
|
|
766
|
+
optionsArg?: INoSqlDbManagementOperationOptions,
|
|
767
|
+
): Promise<INoSqlDbHealth> {
|
|
768
|
+
if (!this.isRunning) {
|
|
769
|
+
return {
|
|
770
|
+
running: false,
|
|
771
|
+
storage: this.options.storage,
|
|
772
|
+
storagePath: this.options.storage === 'file' ? this.options.storagePath : undefined,
|
|
773
|
+
resourceFencingVersion: this.options.storage === 'file' ? 1 : undefined,
|
|
774
|
+
resourceFencingSupported: this.options.storage === 'file' ? false : undefined,
|
|
775
|
+
resourceFencingRequiresDrain: this.options.storage === 'file' ? true : undefined,
|
|
776
|
+
allocationFencingVersion: this.options.storage === 'file' ? 1 : undefined,
|
|
777
|
+
allocationFencingSupported: this.options.storage === 'file' ? false : undefined,
|
|
778
|
+
allocationFencingRequiresDrain: this.options.storage === 'file' ? true : undefined,
|
|
779
|
+
publicationHoldVersion: 1,
|
|
780
|
+
publicationHoldSupported: false,
|
|
781
|
+
publicationHoldRequiresExternalDrain: false,
|
|
782
|
+
authEnabled: Boolean(this.options.auth?.enabled),
|
|
783
|
+
authUsers: this.options.auth?.users?.length ?? 0,
|
|
784
|
+
usersPathConfigured: false,
|
|
785
|
+
databaseCount: 0,
|
|
786
|
+
collectionCount: 0,
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
return this.runDatabaseManagementOperation(
|
|
790
|
+
() => this.bridge.getHealth(optionsArg),
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// --- OpLog / Debug API ---
|
|
795
|
+
|
|
796
|
+
async getCommittedWrites(params: INoSqlDbGetCommittedWritesInput): Promise<INoSqlDbCommittedWritesPage> {
|
|
797
|
+
return this.runDatabaseManagementOperation(() => this.bridge.getCommittedWrites(params));
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Get oplog entries, optionally filtered.
|
|
802
|
+
*/
|
|
803
|
+
async getOpLog(params: {
|
|
804
|
+
sinceSeq?: number;
|
|
805
|
+
limit?: number;
|
|
806
|
+
db?: string;
|
|
807
|
+
collection?: string;
|
|
808
|
+
} = {}): Promise<IOpLogResult> {
|
|
809
|
+
return this.bridge.getOpLog(params);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* Get aggregate oplog statistics.
|
|
814
|
+
*/
|
|
815
|
+
async getOpLogStats(): Promise<IOpLogStats> {
|
|
816
|
+
return this.bridge.getOpLogStats();
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* Revert database state to a specific oplog sequence number.
|
|
821
|
+
* Use dryRun=true to preview which entries would be reverted.
|
|
822
|
+
*/
|
|
823
|
+
async revertToSeq(seq: number, dryRun = false): Promise<IRevertResult> {
|
|
824
|
+
return this.bridge.revertToSeq(seq, dryRun);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* List all collections across all databases, with document counts.
|
|
829
|
+
*/
|
|
830
|
+
async getCollections(db?: string): Promise<ICollectionInfo[]> {
|
|
831
|
+
return this.bridge.getCollections(db);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* Get documents from a collection with pagination.
|
|
836
|
+
*/
|
|
837
|
+
async getDocuments(
|
|
838
|
+
db: string,
|
|
839
|
+
collection: string,
|
|
840
|
+
limit = 50,
|
|
841
|
+
skip = 0,
|
|
842
|
+
): Promise<IDocumentsResult> {
|
|
843
|
+
return this.bridge.getDocuments(db, collection, limit, skip);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* Get server metrics including database/collection counts and oplog info.
|
|
848
|
+
*/
|
|
849
|
+
async getMetrics(): Promise<INoSqlDbMetrics> {
|
|
850
|
+
return this.bridge.getMetrics();
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
private withTenantMongoUri<TDescriptor extends INoSqlDbDatabaseTenantDescriptor>(
|
|
854
|
+
descriptor: TDescriptor,
|
|
855
|
+
password: string,
|
|
856
|
+
): TDescriptor & { mongodbUri: string } {
|
|
857
|
+
return {
|
|
858
|
+
...descriptor,
|
|
859
|
+
mongodbUri: this.buildTenantMongoUri(descriptor.databaseName, descriptor.username, password),
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
private buildTenantMongoUri(databaseName: string, username: string, password: string): string {
|
|
864
|
+
const host = this.options.socketPath
|
|
865
|
+
? encodeURIComponent(this.options.socketPath)
|
|
866
|
+
: `${this.options.host ?? '127.0.0.1'}:${this.port}`;
|
|
867
|
+
const auth = `${encodeURIComponent(username)}:${encodeURIComponent(password)}@`;
|
|
868
|
+
const query = new URLSearchParams({ authSource: databaseName });
|
|
869
|
+
if (this.options.tls?.enabled) {
|
|
870
|
+
query.set('tls', 'true');
|
|
871
|
+
}
|
|
872
|
+
return `mongodb://${auth}${host}/${encodeURIComponent(databaseName)}?${query.toString()}`;
|
|
873
|
+
}
|
|
874
|
+
}
|