@push.rocks/smartdb 2.18.1 → 3.0.1
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_rust/rustdb_linux_amd64 +0 -0
- package/dist_rust/rustdb_linux_arm64 +0 -0
- package/dist_ts/00_commitinfo_data.js +2 -2
- package/dist_ts/index.d.ts +1 -1
- package/dist_ts/index.js +1 -1
- package/dist_ts/ts_migration/classes.authmetadatamigrationrunner.d.ts +2 -1
- package/dist_ts/ts_migration/classes.authmetadatamigrationrunner.js +3 -3
- package/dist_ts/ts_migration/classes.storagemigrator.d.ts +2 -1
- package/dist_ts/ts_migration/classes.storagemigrator.js +36 -20
- package/dist_ts/ts_migration/index.d.ts +1 -0
- package/dist_ts/ts_migration/index.js +2 -1
- package/dist_ts/ts_migration/migrators/v0_to_v1.d.ts +7 -1
- package/dist_ts/ts_migration/migrators/v0_to_v1.js +162 -47
- package/dist_ts/ts_migration/migrators/v1_auth_metadata_permissions.d.ts +3 -2
- package/dist_ts/ts_migration/migrators/v1_auth_metadata_permissions.js +3 -3
- package/dist_ts/ts_smartdb/index.d.ts +1 -1
- package/dist_ts/ts_smartdb/index.js +1 -1
- package/dist_ts/ts_smartdb/resource-fencing.d.ts +12 -1
- package/dist_ts/ts_smartdb/resource-fencing.js +44 -2
- package/dist_ts/ts_smartdb/rust-db-bridge.d.ts +5 -4
- package/dist_ts/ts_smartdb/rust-db-bridge.js +33 -10
- package/dist_ts/ts_smartdb/server/SmartdbServer.d.ts +4 -2
- package/dist_ts/ts_smartdb/server/SmartdbServer.js +84 -9
- package/dist_ts/ts_smartdb/service-types.d.ts +47 -2
- package/dist_ts/ts_smartdb/service-types.js +7 -2
- package/package.json +1 -1
- package/readme.md +19 -4
- package/readme.plan.md +2 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/index.ts +4 -0
- package/ts/ts_migration/classes.authmetadatamigrationrunner.ts +3 -1
- package/ts/ts_migration/classes.storagemigrator.ts +38 -19
- package/ts/ts_migration/index.ts +1 -0
- package/ts/ts_migration/migrators/v0_to_v1.ts +224 -44
- package/ts/ts_migration/migrators/v1_auth_metadata_permissions.ts +4 -0
- package/ts/ts_smartdb/index.ts +4 -0
- package/ts/ts_smartdb/resource-fencing.ts +59 -2
- package/ts/ts_smartdb/rust-db-bridge.ts +56 -11
- package/ts/ts_smartdb/server/SmartdbServer.ts +105 -9
- package/ts/ts_smartdb/service-types.ts +59 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
import * as crypto from 'crypto';
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import * as crypto from 'node:crypto';
|
|
4
4
|
import { BSON } from 'bson';
|
|
5
5
|
|
|
6
6
|
// ---------------------------------------------------------------------------
|
|
@@ -100,6 +100,141 @@ function crc32(data: Buffer): number {
|
|
|
100
100
|
return (crc ^ 0xFFFFFFFF) >>> 0;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
const pathExists = async (pathArg: string): Promise<boolean> => {
|
|
104
|
+
try {
|
|
105
|
+
await fs.stat(pathArg);
|
|
106
|
+
return true;
|
|
107
|
+
} catch (error) {
|
|
108
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false;
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export class SmartDbStorageMigrationCleanupError extends AggregateError {
|
|
114
|
+
constructor(operationErrorArg: unknown, cleanupErrorsArg: unknown[]) {
|
|
115
|
+
super(
|
|
116
|
+
[operationErrorArg, ...cleanupErrorsArg],
|
|
117
|
+
'SmartDB legacy storage migration failed and staging cleanup was incomplete',
|
|
118
|
+
{ cause: operationErrorArg },
|
|
119
|
+
);
|
|
120
|
+
this.name = 'SmartDbStorageMigrationCleanupError';
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export const runWithOwnedMigrationFileHandle = async <
|
|
125
|
+
TResult,
|
|
126
|
+
TFileHandle extends { close(): Promise<void> },
|
|
127
|
+
>(
|
|
128
|
+
fileHandleArg: TFileHandle,
|
|
129
|
+
operationArg: (
|
|
130
|
+
fileHandleArg: TFileHandle,
|
|
131
|
+
) => Promise<TResult>,
|
|
132
|
+
signalArg?: AbortSignal,
|
|
133
|
+
): Promise<TResult> => {
|
|
134
|
+
let operationResult: TResult | undefined;
|
|
135
|
+
let operationError: unknown;
|
|
136
|
+
let operationCompleted = false;
|
|
137
|
+
try {
|
|
138
|
+
operationResult = await operationArg(fileHandleArg);
|
|
139
|
+
operationCompleted = true;
|
|
140
|
+
} catch (error) {
|
|
141
|
+
operationError = error;
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
await fileHandleArg.close();
|
|
145
|
+
} catch (closeError) {
|
|
146
|
+
if (operationError !== undefined) {
|
|
147
|
+
throw new SmartDbStorageMigrationCleanupError(operationError, [closeError]);
|
|
148
|
+
}
|
|
149
|
+
if (signalArg?.aborted) {
|
|
150
|
+
throw new SmartDbStorageMigrationCleanupError(signalArg.reason, [closeError]);
|
|
151
|
+
}
|
|
152
|
+
throw closeError;
|
|
153
|
+
}
|
|
154
|
+
if (!operationCompleted) throw operationError;
|
|
155
|
+
return operationResult as TResult;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const withFileHandle = async <TResult>(
|
|
159
|
+
filePathArg: string,
|
|
160
|
+
flagsArg: 'r' | 'wx',
|
|
161
|
+
operationArg: (
|
|
162
|
+
fileHandleArg: Awaited<ReturnType<typeof fs.open>>,
|
|
163
|
+
) => Promise<TResult>,
|
|
164
|
+
signalArg?: AbortSignal,
|
|
165
|
+
): Promise<TResult> => runWithOwnedMigrationFileHandle(
|
|
166
|
+
await fs.open(filePathArg, flagsArg),
|
|
167
|
+
operationArg,
|
|
168
|
+
signalArg,
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const writeBuffer = async (
|
|
172
|
+
fileHandleArg: Awaited<ReturnType<typeof fs.open>>,
|
|
173
|
+
bufferArg: Buffer,
|
|
174
|
+
signalArg?: AbortSignal,
|
|
175
|
+
): Promise<void> => {
|
|
176
|
+
let offset = 0;
|
|
177
|
+
while (offset < bufferArg.length) {
|
|
178
|
+
signalArg?.throwIfAborted();
|
|
179
|
+
const { bytesWritten } = await fileHandleArg.write(
|
|
180
|
+
bufferArg,
|
|
181
|
+
offset,
|
|
182
|
+
bufferArg.length - offset,
|
|
183
|
+
null,
|
|
184
|
+
);
|
|
185
|
+
if (bytesWritten <= 0) {
|
|
186
|
+
throw new Error('SmartDB storage migration write made no progress');
|
|
187
|
+
}
|
|
188
|
+
offset += bytesWritten;
|
|
189
|
+
}
|
|
190
|
+
signalArg?.throwIfAborted();
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const syncDirectory = async (
|
|
194
|
+
directoryPathArg: string,
|
|
195
|
+
signalArg?: AbortSignal,
|
|
196
|
+
): Promise<void> => {
|
|
197
|
+
try {
|
|
198
|
+
await withFileHandle(directoryPathArg, 'r', async (directoryHandleArg) => {
|
|
199
|
+
await directoryHandleArg.sync();
|
|
200
|
+
}, signalArg);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
if (!['EINVAL', 'ENOTSUP'].includes((error as NodeJS.ErrnoException).code || '')) {
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const syncFile = async (filePathArg: string, signalArg?: AbortSignal): Promise<void> => {
|
|
209
|
+
await withFileHandle(filePathArg, 'r', async (fileHandleArg) => {
|
|
210
|
+
await fileHandleArg.sync();
|
|
211
|
+
}, signalArg);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const assertPublishedMigrationComplete = async (
|
|
215
|
+
collectionDirectoryArg: string,
|
|
216
|
+
): Promise<void> => {
|
|
217
|
+
for (const fileName of ['data.rdb', 'keydir.hint', 'indexes.json']) {
|
|
218
|
+
let fileStat: Awaited<ReturnType<typeof fs.lstat>>;
|
|
219
|
+
try {
|
|
220
|
+
fileStat = await fs.lstat(path.join(collectionDirectoryArg, fileName));
|
|
221
|
+
} catch (error) {
|
|
222
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
223
|
+
throw new Error(
|
|
224
|
+
`SmartDB legacy migration target is incomplete: ${collectionDirectoryArg}`,
|
|
225
|
+
{ cause: error },
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
if (!fileStat.isFile() || fileStat.isSymbolicLink()) {
|
|
231
|
+
throw new Error(
|
|
232
|
+
`SmartDB legacy migration target is incomplete: ${collectionDirectoryArg}`,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
103
238
|
// ---------------------------------------------------------------------------
|
|
104
239
|
// Migration: v0 (JSON) → v1 (Bitcask binary)
|
|
105
240
|
// ---------------------------------------------------------------------------
|
|
@@ -119,21 +254,27 @@ interface IKeyDirEntry {
|
|
|
119
254
|
* - Returns a list of old files that can be safely deleted.
|
|
120
255
|
* - On failure, cleans up any partial new files and throws.
|
|
121
256
|
*/
|
|
122
|
-
export async function migrateV0ToV1(
|
|
257
|
+
export async function migrateV0ToV1(
|
|
258
|
+
storagePath: string,
|
|
259
|
+
signalArg?: AbortSignal,
|
|
260
|
+
): Promise<string[]> {
|
|
123
261
|
const deletableFiles: string[] = [];
|
|
124
|
-
const
|
|
262
|
+
const stagingDirectories = new Set<string>();
|
|
125
263
|
|
|
126
264
|
try {
|
|
127
|
-
|
|
265
|
+
signalArg?.throwIfAborted();
|
|
266
|
+
const dbEntries = await fs.readdir(storagePath, { withFileTypes: true });
|
|
128
267
|
|
|
129
268
|
for (const dbEntry of dbEntries) {
|
|
269
|
+
signalArg?.throwIfAborted();
|
|
130
270
|
if (!dbEntry.isDirectory()) continue;
|
|
131
271
|
if (dbEntry.name.startsWith('.__rustdb_')) continue;
|
|
132
272
|
|
|
133
273
|
const dbDir = path.join(storagePath, dbEntry.name);
|
|
134
|
-
const collFiles = fs.
|
|
274
|
+
const collFiles = await fs.readdir(dbDir, { withFileTypes: true });
|
|
135
275
|
|
|
136
276
|
for (const collFile of collFiles) {
|
|
277
|
+
signalArg?.throwIfAborted();
|
|
137
278
|
if (!collFile.isFile()) continue;
|
|
138
279
|
if (collFile.name.startsWith('.__rustdb_')) continue;
|
|
139
280
|
if (!collFile.name.endsWith('.json')) continue;
|
|
@@ -145,35 +286,46 @@ export async function migrateV0ToV1(storagePath: string): Promise<string[]> {
|
|
|
145
286
|
|
|
146
287
|
// Target directory
|
|
147
288
|
const collDir = path.join(dbDir, collName);
|
|
148
|
-
if (
|
|
149
|
-
|
|
289
|
+
if (await pathExists(collDir)) {
|
|
290
|
+
await assertPublishedMigrationComplete(collDir);
|
|
291
|
+
if (await pathExists(indexJsonPath)) deletableFiles.push(indexJsonPath);
|
|
292
|
+
deletableFiles.push(jsonPath);
|
|
150
293
|
continue;
|
|
151
294
|
}
|
|
152
295
|
|
|
153
296
|
console.log(`[smartdb] Migrating ${dbEntry.name}.${collName}...`);
|
|
154
297
|
|
|
155
298
|
// Read the JSON collection
|
|
156
|
-
const jsonData = fs.
|
|
299
|
+
const jsonData = await fs.readFile(jsonPath, {
|
|
300
|
+
encoding: 'utf8',
|
|
301
|
+
signal: signalArg,
|
|
302
|
+
});
|
|
303
|
+
signalArg?.throwIfAborted();
|
|
157
304
|
const docs: any[] = JSON.parse(jsonData);
|
|
158
305
|
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
|
|
306
|
+
// Build the complete collection in a hidden sibling and publish it
|
|
307
|
+
// atomically so cancellation can never expose partial v1 state.
|
|
308
|
+
const stagingDir = path.join(
|
|
309
|
+
dbDir,
|
|
310
|
+
`.__rustdb_migration_${collName}_${process.pid}_${crypto.randomBytes(8).toString('hex')}`,
|
|
311
|
+
);
|
|
312
|
+
await fs.mkdir(stagingDir);
|
|
313
|
+
stagingDirectories.add(stagingDir);
|
|
314
|
+
signalArg?.throwIfAborted();
|
|
162
315
|
|
|
163
316
|
// Write data.rdb
|
|
164
|
-
const dataPath = path.join(
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
try {
|
|
317
|
+
const dataPath = path.join(stagingDir, 'data.rdb');
|
|
318
|
+
await withFileHandle(dataPath, 'wx', async (dataFileHandle) => {
|
|
168
319
|
// File header
|
|
169
320
|
const headerBuf = writeFileHeader(FILE_TYPE_DATA);
|
|
170
|
-
|
|
321
|
+
await writeBuffer(dataFileHandle, headerBuf, signalArg);
|
|
171
322
|
|
|
172
323
|
let currentOffset = BigInt(FILE_HEADER_SIZE);
|
|
173
324
|
const keydir: Map<string, IKeyDirEntry> = new Map();
|
|
174
325
|
const ts = BigInt(Date.now());
|
|
175
326
|
|
|
176
327
|
for (const doc of docs) {
|
|
328
|
+
signalArg?.throwIfAborted();
|
|
177
329
|
// Extract _id
|
|
178
330
|
let idHex: string;
|
|
179
331
|
if (doc._id && doc._id.$oid) {
|
|
@@ -194,7 +346,7 @@ export async function migrateV0ToV1(storagePath: string): Promise<string[]> {
|
|
|
194
346
|
const valueBuf = Buffer.from(bsonBytes);
|
|
195
347
|
|
|
196
348
|
const record = encodeDataRecord(ts, keyBuf, valueBuf);
|
|
197
|
-
|
|
349
|
+
await writeBuffer(dataFileHandle, record, signalArg);
|
|
198
350
|
|
|
199
351
|
keydir.set(idHex, {
|
|
200
352
|
offset: currentOffset,
|
|
@@ -206,47 +358,75 @@ export async function migrateV0ToV1(storagePath: string): Promise<string[]> {
|
|
|
206
358
|
currentOffset += BigInt(record.length);
|
|
207
359
|
}
|
|
208
360
|
|
|
209
|
-
|
|
210
|
-
|
|
361
|
+
await dataFileHandle.sync();
|
|
362
|
+
signalArg?.throwIfAborted();
|
|
211
363
|
|
|
212
364
|
// Write keydir.hint
|
|
213
|
-
const hintPath = path.join(
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
365
|
+
const hintPath = path.join(stagingDir, 'keydir.hint');
|
|
366
|
+
await withFileHandle(hintPath, 'wx', async (hintFileHandle) => {
|
|
367
|
+
await writeBuffer(hintFileHandle, writeFileHeader(FILE_TYPE_HINT), signalArg);
|
|
368
|
+
for (const [key, entry] of keydir) {
|
|
369
|
+
signalArg?.throwIfAborted();
|
|
370
|
+
await writeBuffer(
|
|
371
|
+
hintFileHandle,
|
|
372
|
+
encodeHintEntry(
|
|
373
|
+
key,
|
|
374
|
+
entry.offset,
|
|
375
|
+
entry.recordLen,
|
|
376
|
+
entry.valueLen,
|
|
377
|
+
entry.timestamp,
|
|
378
|
+
),
|
|
379
|
+
signalArg,
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
await hintFileHandle.sync();
|
|
383
|
+
signalArg?.throwIfAborted();
|
|
384
|
+
}, signalArg);
|
|
385
|
+
}, signalArg);
|
|
227
386
|
|
|
228
387
|
// Copy indexes.json if it exists
|
|
229
|
-
if (
|
|
230
|
-
const destIndexPath = path.join(
|
|
231
|
-
fs.
|
|
388
|
+
if (await pathExists(indexJsonPath)) {
|
|
389
|
+
const destIndexPath = path.join(stagingDir, 'indexes.json');
|
|
390
|
+
await fs.copyFile(indexJsonPath, destIndexPath);
|
|
391
|
+
await syncFile(destIndexPath, signalArg);
|
|
392
|
+
signalArg?.throwIfAborted();
|
|
232
393
|
deletableFiles.push(indexJsonPath);
|
|
233
394
|
} else {
|
|
234
395
|
// Write default _id index
|
|
235
|
-
const destIndexPath = path.join(
|
|
236
|
-
fs.
|
|
396
|
+
const destIndexPath = path.join(stagingDir, 'indexes.json');
|
|
397
|
+
await fs.writeFile(
|
|
398
|
+
destIndexPath,
|
|
399
|
+
JSON.stringify([{ name: '_id_', key: { _id: 1 } }], null, 2),
|
|
400
|
+
{ signal: signalArg },
|
|
401
|
+
);
|
|
402
|
+
await syncFile(destIndexPath, signalArg);
|
|
403
|
+
signalArg?.throwIfAborted();
|
|
237
404
|
}
|
|
238
405
|
|
|
406
|
+
await assertPublishedMigrationComplete(stagingDir);
|
|
407
|
+
await syncDirectory(stagingDir, signalArg);
|
|
408
|
+
signalArg?.throwIfAborted();
|
|
409
|
+
await fs.rename(stagingDir, collDir);
|
|
410
|
+
stagingDirectories.delete(stagingDir);
|
|
411
|
+
await syncDirectory(dbDir, signalArg);
|
|
412
|
+
signalArg?.throwIfAborted();
|
|
239
413
|
deletableFiles.push(jsonPath);
|
|
240
414
|
|
|
241
415
|
console.log(`[smartdb] Migrated ${dbEntry.name}.${collName}: ${docs.length} documents`);
|
|
242
416
|
}
|
|
243
417
|
}
|
|
244
418
|
} catch (err) {
|
|
245
|
-
|
|
246
|
-
for (const dir of
|
|
419
|
+
const cleanupErrors: unknown[] = [];
|
|
420
|
+
for (const dir of Array.from(stagingDirectories).reverse()) {
|
|
247
421
|
try {
|
|
248
|
-
fs.
|
|
249
|
-
|
|
422
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
423
|
+
await syncDirectory(path.dirname(dir));
|
|
424
|
+
} catch (cleanupError) {
|
|
425
|
+
cleanupErrors.push(cleanupError);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (cleanupErrors.length > 0) {
|
|
429
|
+
throw new SmartDbStorageMigrationCleanupError(err, cleanupErrors);
|
|
250
430
|
}
|
|
251
431
|
throw err;
|
|
252
432
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { TAuthMetadataPermissionTransitionStatus } from '../../ts_smartdb/rust-db-bridge.js';
|
|
2
|
+
import type { ISmartDbManagementOperationOptions } from '../../ts_smartdb/service-types.js';
|
|
2
3
|
|
|
3
4
|
const legacyAuthMetadataMode = 0o644;
|
|
4
5
|
const privateAuthMetadataMode = 0o600;
|
|
@@ -8,6 +9,7 @@ export interface IAuthMetadataPermissionMigrationBridge {
|
|
|
8
9
|
usersPathArg: string,
|
|
9
10
|
expectedModeArg: number,
|
|
10
11
|
targetModeArg: number,
|
|
12
|
+
optionsArg?: ISmartDbManagementOperationOptions,
|
|
11
13
|
): Promise<TAuthMetadataPermissionTransitionStatus>;
|
|
12
14
|
}
|
|
13
15
|
|
|
@@ -22,9 +24,11 @@ export type TAuthMetadataPermissionMigrationStatus =
|
|
|
22
24
|
export const migrateV1AuthMetadataPermissions = async (
|
|
23
25
|
bridgeArg: IAuthMetadataPermissionMigrationBridge,
|
|
24
26
|
usersPathArg: string,
|
|
27
|
+
optionsArg?: ISmartDbManagementOperationOptions,
|
|
25
28
|
): Promise<TAuthMetadataPermissionMigrationStatus> => bridgeArg
|
|
26
29
|
.transitionAuthMetadataPermissions(
|
|
27
30
|
usersPathArg,
|
|
28
31
|
legacyAuthMetadataMode,
|
|
29
32
|
privateAuthMetadataMode,
|
|
33
|
+
optionsArg,
|
|
30
34
|
);
|
package/ts/ts_smartdb/index.ts
CHANGED
|
@@ -36,6 +36,8 @@ export type {
|
|
|
36
36
|
ISmartDbCommitDatabasePublicationInput,
|
|
37
37
|
ISmartDbManagementOperationOptions,
|
|
38
38
|
ISmartDbDatabaseResourceFenceState,
|
|
39
|
+
ISmartDbDatabaseAllocationIdentity,
|
|
40
|
+
ISmartDbDatabaseAllocationLifecycle,
|
|
39
41
|
ILocalSmartDbOfflineInspectionLimits,
|
|
40
42
|
ILocalSmartDbOfflineStringValueInspection,
|
|
41
43
|
TLocalSmartDbOfflineStringValueInspectionResult,
|
|
@@ -44,6 +46,8 @@ export type {
|
|
|
44
46
|
ISmartDbDatabaseTenantInput,
|
|
45
47
|
ISmartDbEnsureDatabaseTenantInput,
|
|
46
48
|
ISmartDbEnsureDatabaseTenantResult,
|
|
49
|
+
ISmartDbAllocateDatabaseTenantInput,
|
|
50
|
+
ISmartDbAllocateDatabaseTenantResult,
|
|
47
51
|
ISmartDbDeleteDatabaseTenantInput,
|
|
48
52
|
ISmartDbRotateDatabaseTenantPasswordInput,
|
|
49
53
|
ISmartDbDatabaseTenantDescriptor,
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
ISmartDbDatabaseAllocationIdentity,
|
|
4
|
+
ISmartDbDatabaseExport,
|
|
5
|
+
} from './service-types.js';
|
|
3
6
|
|
|
4
7
|
export type TSmartDbEffectivePayloadInputV1 =
|
|
5
8
|
| {
|
|
@@ -9,6 +12,7 @@ export type TSmartDbEffectivePayloadInputV1 =
|
|
|
9
12
|
source: ISmartDbDatabaseExport;
|
|
10
13
|
/** Exact exclusive tenant owner; omission retains the legacy v1 digest. */
|
|
11
14
|
username?: string;
|
|
15
|
+
allocation?: ISmartDbDatabaseAllocationIdentity;
|
|
12
16
|
holdPublication?: boolean;
|
|
13
17
|
}
|
|
14
18
|
| {
|
|
@@ -16,6 +20,7 @@ export type TSmartDbEffectivePayloadInputV1 =
|
|
|
16
20
|
kind: 'smartdb.database.delete.v1';
|
|
17
21
|
databaseName: string;
|
|
18
22
|
username?: string;
|
|
23
|
+
allocation?: ISmartDbDatabaseAllocationIdentity;
|
|
19
24
|
holdPublication?: boolean;
|
|
20
25
|
}
|
|
21
26
|
| {
|
|
@@ -25,6 +30,16 @@ export type TSmartDbEffectivePayloadInputV1 =
|
|
|
25
30
|
username: string;
|
|
26
31
|
password: string;
|
|
27
32
|
roles?: string[];
|
|
33
|
+
allocation?: ISmartDbDatabaseAllocationIdentity;
|
|
34
|
+
}
|
|
35
|
+
| {
|
|
36
|
+
version: 1;
|
|
37
|
+
kind: 'smartdb.database.allocate.v1';
|
|
38
|
+
databaseName: string;
|
|
39
|
+
expectedAbsent: true;
|
|
40
|
+
username: string;
|
|
41
|
+
password: string;
|
|
42
|
+
roles?: string[];
|
|
28
43
|
};
|
|
29
44
|
|
|
30
45
|
type TCanonicalJsonValue =
|
|
@@ -228,12 +243,17 @@ export const createSmartDbEffectivePayloadSha256V1 = (
|
|
|
228
243
|
...(inputArg.username === undefined
|
|
229
244
|
? {}
|
|
230
245
|
: { username: inputArg.username }),
|
|
246
|
+
...(inputArg.allocation === undefined
|
|
247
|
+
? {}
|
|
248
|
+
: { allocation: inputArg.allocation }),
|
|
231
249
|
...(inputArg.holdPublication === true
|
|
232
250
|
? { holdPublication: true }
|
|
233
251
|
: {}),
|
|
234
252
|
};
|
|
235
253
|
useTypedEncoding =
|
|
236
|
-
inputArg.username !== undefined ||
|
|
254
|
+
inputArg.username !== undefined ||
|
|
255
|
+
inputArg.allocation !== undefined ||
|
|
256
|
+
inputArg.holdPublication === true;
|
|
237
257
|
break;
|
|
238
258
|
case 'smartdb.database.ensure.v1': {
|
|
239
259
|
if (typeof inputArg.username !== 'string') {
|
|
@@ -264,7 +284,40 @@ export const createSmartDbEffectivePayloadSha256V1 = (
|
|
|
264
284
|
.update(inputArg.password, 'utf8')
|
|
265
285
|
.digest('hex'),
|
|
266
286
|
roles: canonicalRoles,
|
|
287
|
+
...(inputArg.allocation === undefined
|
|
288
|
+
? {}
|
|
289
|
+
: { allocation: inputArg.allocation }),
|
|
267
290
|
};
|
|
291
|
+
useTypedEncoding = inputArg.allocation !== undefined;
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
case 'smartdb.database.allocate.v1': {
|
|
295
|
+
if (inputArg.expectedAbsent !== true) {
|
|
296
|
+
throw new TypeError('SmartDB allocation expectedAbsent must be true');
|
|
297
|
+
}
|
|
298
|
+
if (typeof inputArg.username !== 'string') {
|
|
299
|
+
throw new TypeError('SmartDB allocation username must be a string');
|
|
300
|
+
}
|
|
301
|
+
if (typeof inputArg.password !== 'string') {
|
|
302
|
+
throw new TypeError('SmartDB allocation password must be a string');
|
|
303
|
+
}
|
|
304
|
+
const roles = inputArg.roles ?? ['readWrite', 'dbAdmin'];
|
|
305
|
+
if (!Array.isArray(roles) || roles.some((roleArg) => typeof roleArg !== 'string' || roleArg.length === 0)) {
|
|
306
|
+
throw new TypeError('SmartDB allocation roles must be an array of non-empty strings');
|
|
307
|
+
}
|
|
308
|
+
effectivePayload = {
|
|
309
|
+
version: 1,
|
|
310
|
+
kind: inputArg.kind,
|
|
311
|
+
databaseName: inputArg.databaseName,
|
|
312
|
+
expectedAbsent: true,
|
|
313
|
+
username: inputArg.username,
|
|
314
|
+
passwordSha256: plugins.crypto
|
|
315
|
+
.createHash('sha256')
|
|
316
|
+
.update(inputArg.password, 'utf8')
|
|
317
|
+
.digest('hex'),
|
|
318
|
+
roles: [...new Set(roles)].sort(compareUtf8Keys),
|
|
319
|
+
};
|
|
320
|
+
useTypedEncoding = true;
|
|
268
321
|
break;
|
|
269
322
|
}
|
|
270
323
|
case 'smartdb.database.delete.v1':
|
|
@@ -287,10 +340,14 @@ export const createSmartDbEffectivePayloadSha256V1 = (
|
|
|
287
340
|
...(inputArg.username === undefined
|
|
288
341
|
? { rawDatabaseOnly: true }
|
|
289
342
|
: { username: inputArg.username, rawDatabaseOnly: false }),
|
|
343
|
+
...(inputArg.allocation === undefined
|
|
344
|
+
? {}
|
|
345
|
+
: { allocation: inputArg.allocation }),
|
|
290
346
|
...(inputArg.holdPublication === true
|
|
291
347
|
? { holdPublication: true }
|
|
292
348
|
: {}),
|
|
293
349
|
};
|
|
350
|
+
useTypedEncoding = inputArg.allocation !== undefined;
|
|
294
351
|
break;
|
|
295
352
|
default:
|
|
296
353
|
throw new TypeError('SmartDB mutation kind has no version 1 payload profile');
|
|
@@ -12,6 +12,8 @@ import type {
|
|
|
12
12
|
ISmartDbDatabaseTenantInput,
|
|
13
13
|
ISmartDbEnsureDatabaseTenantInput,
|
|
14
14
|
ISmartDbEnsureDatabaseTenantResult,
|
|
15
|
+
ISmartDbAllocateDatabaseTenantInput,
|
|
16
|
+
ISmartDbAllocateDatabaseTenantResult,
|
|
15
17
|
ISmartDbDeleteDatabaseTenantInput,
|
|
16
18
|
ISmartDbRotateDatabaseTenantPasswordInput,
|
|
17
19
|
ISmartDbDatabaseTenantDescriptor,
|
|
@@ -35,6 +37,8 @@ export type {
|
|
|
35
37
|
ISmartDbDatabaseTenantInput,
|
|
36
38
|
ISmartDbEnsureDatabaseTenantInput,
|
|
37
39
|
ISmartDbEnsureDatabaseTenantResult,
|
|
40
|
+
ISmartDbAllocateDatabaseTenantInput,
|
|
41
|
+
ISmartDbAllocateDatabaseTenantResult,
|
|
38
42
|
ISmartDbDeleteDatabaseTenantInput,
|
|
39
43
|
ISmartDbRotateDatabaseTenantPasswordInput,
|
|
40
44
|
ISmartDbDatabaseTenantDescriptor,
|
|
@@ -167,6 +171,10 @@ type TSmartDbCommands = {
|
|
|
167
171
|
params: ISmartDbDatabaseTenantInput;
|
|
168
172
|
result: ISmartDbDatabaseTenantDescriptor;
|
|
169
173
|
};
|
|
174
|
+
allocateDatabaseTenant: {
|
|
175
|
+
params: ISmartDbAllocateDatabaseTenantInput;
|
|
176
|
+
result: ISmartDbAllocateDatabaseTenantResult;
|
|
177
|
+
};
|
|
170
178
|
ensureDatabaseTenant: {
|
|
171
179
|
params: ISmartDbEnsureDatabaseTenantInput;
|
|
172
180
|
result: ISmartDbEnsureDatabaseTenantResult;
|
|
@@ -529,20 +537,41 @@ export class RustDbBridge extends EventEmitter {
|
|
|
529
537
|
usersPathArg: string,
|
|
530
538
|
expectedModeArg: number,
|
|
531
539
|
targetModeArg: number,
|
|
540
|
+
optionsArg?: ISmartDbManagementOperationOptions,
|
|
532
541
|
): Promise<TAuthMetadataPermissionTransitionStatus> {
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
+
validateSmartDbManagementOperationOptions(optionsArg);
|
|
543
|
+
try {
|
|
544
|
+
const result = await this.bridge.sendCommand(
|
|
545
|
+
'transitionAuthMetadataPermissions',
|
|
546
|
+
{
|
|
547
|
+
usersPath: usersPathArg,
|
|
548
|
+
expectedMode: expectedModeArg,
|
|
549
|
+
targetMode: targetModeArg,
|
|
550
|
+
},
|
|
551
|
+
optionsArg,
|
|
552
|
+
) as { status: TAuthMetadataPermissionTransitionStatus };
|
|
553
|
+
return result.status;
|
|
554
|
+
} catch (error) {
|
|
555
|
+
await this.awaitManagementOperationTermination(error);
|
|
556
|
+
throw error;
|
|
557
|
+
}
|
|
542
558
|
}
|
|
543
559
|
|
|
544
|
-
public async startDb(
|
|
545
|
-
|
|
560
|
+
public async startDb(
|
|
561
|
+
config: ISmartDbRustConfig,
|
|
562
|
+
optionsArg?: ISmartDbManagementOperationOptions,
|
|
563
|
+
): Promise<{ connectionUri: string }> {
|
|
564
|
+
validateSmartDbManagementOperationOptions(optionsArg);
|
|
565
|
+
try {
|
|
566
|
+
return await this.bridge.sendCommand(
|
|
567
|
+
'start',
|
|
568
|
+
{ config },
|
|
569
|
+
optionsArg,
|
|
570
|
+
) as { connectionUri: string };
|
|
571
|
+
} catch (error) {
|
|
572
|
+
await this.awaitManagementOperationTermination(error);
|
|
573
|
+
throw error;
|
|
574
|
+
}
|
|
546
575
|
}
|
|
547
576
|
|
|
548
577
|
public async stopDb(): Promise<void> {
|
|
@@ -598,6 +627,22 @@ export class RustDbBridge extends EventEmitter {
|
|
|
598
627
|
}
|
|
599
628
|
}
|
|
600
629
|
|
|
630
|
+
public async allocateDatabaseTenant(
|
|
631
|
+
params: ISmartDbAllocateDatabaseTenantInput,
|
|
632
|
+
optionsArg?: ISmartDbManagementOperationOptions,
|
|
633
|
+
): Promise<ISmartDbAllocateDatabaseTenantResult> {
|
|
634
|
+
try {
|
|
635
|
+
return await this.bridge.sendCommand(
|
|
636
|
+
'allocateDatabaseTenant',
|
|
637
|
+
params,
|
|
638
|
+
optionsArg,
|
|
639
|
+
) as ISmartDbAllocateDatabaseTenantResult;
|
|
640
|
+
} catch (error) {
|
|
641
|
+
await this.awaitManagementOperationTermination(error);
|
|
642
|
+
throw normalizeSmartDbResourceFenceError(error);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
601
646
|
public async deleteDatabaseTenant(
|
|
602
647
|
params: ISmartDbDeleteDatabaseTenantInput,
|
|
603
648
|
optionsArg?: ISmartDbManagementOperationOptions,
|