@objectql/driver-mongo 4.0.2 → 4.0.4
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/.turbo/turbo-build.log +4 -0
- package/CHANGELOG.md +58 -0
- package/dist/index.d.ts +118 -1
- package/dist/index.js +266 -11
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +320 -14
- package/test/index.test.ts +15 -13
- package/test/integration.test.ts +260 -15
- package/test/tck.test.ts +73 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { Data,
|
|
1
|
+
import { Data, System as SystemSpec } from '@objectstack/spec';
|
|
2
2
|
type QueryAST = Data.QueryAST;
|
|
3
3
|
type SortNode = Data.SortNode;
|
|
4
|
-
type DriverInterface =
|
|
4
|
+
type DriverInterface = Data.DriverInterface;
|
|
5
5
|
/**
|
|
6
6
|
* ObjectQL
|
|
7
7
|
* Copyright (c) 2026-present ObjectStack Inc.
|
|
@@ -11,7 +11,24 @@ type DriverInterface = DriverSpec.DriverInterface;
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { Driver } from '@objectql/types';
|
|
14
|
-
import { MongoClient, Db, Filter, ObjectId, FindOptions } from 'mongodb';
|
|
14
|
+
import { MongoClient, Db, Filter, ObjectId, FindOptions, FindOneAndUpdateOptions, UpdateFilter, ChangeStream, ChangeStreamDocument } from 'mongodb';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Change stream event handler callback
|
|
18
|
+
*/
|
|
19
|
+
export type ChangeStreamHandler = (change: ChangeStreamDocument) => void | Promise<void>;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Change stream options
|
|
23
|
+
*/
|
|
24
|
+
export interface ChangeStreamOptions {
|
|
25
|
+
/** Filter for specific operation types (insert, update, delete, replace) */
|
|
26
|
+
operationTypes?: ('insert' | 'update' | 'delete' | 'replace')[];
|
|
27
|
+
/** Full document lookup for update operations */
|
|
28
|
+
fullDocument?: 'updateLookup' | 'whenAvailable' | 'required';
|
|
29
|
+
/** Pipeline to filter change events */
|
|
30
|
+
pipeline?: any[];
|
|
31
|
+
}
|
|
15
32
|
|
|
16
33
|
/**
|
|
17
34
|
* Command interface for executeCommand method
|
|
@@ -68,11 +85,13 @@ export class MongoDriver implements Driver {
|
|
|
68
85
|
private db?: Db;
|
|
69
86
|
private config: any;
|
|
70
87
|
private connected: Promise<void>;
|
|
88
|
+
private changeStreams: Map<string, ChangeStream>;
|
|
71
89
|
|
|
72
90
|
constructor(config: { url: string, dbName?: string }) {
|
|
73
91
|
this.config = config;
|
|
74
92
|
this.client = new MongoClient(config.url);
|
|
75
93
|
this.connected = this.internalConnect();
|
|
94
|
+
this.changeStreams = new Map();
|
|
76
95
|
}
|
|
77
96
|
|
|
78
97
|
/**
|
|
@@ -391,7 +410,22 @@ export class MongoDriver implements Driver {
|
|
|
391
410
|
mongoDoc._id = new ObjectId().toHexString();
|
|
392
411
|
}
|
|
393
412
|
|
|
394
|
-
|
|
413
|
+
// Add timestamps if not already present
|
|
414
|
+
const now = new Date().toISOString();
|
|
415
|
+
if (!mongoDoc.created_at) {
|
|
416
|
+
mongoDoc.created_at = now;
|
|
417
|
+
}
|
|
418
|
+
if (!mongoDoc.updated_at) {
|
|
419
|
+
mongoDoc.updated_at = now;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Pass session for transactional operations only if it exists
|
|
423
|
+
// Support both 'transaction' and 'session' option keys for compatibility
|
|
424
|
+
const session = options?.transaction || options?.session;
|
|
425
|
+
const result = session
|
|
426
|
+
? await collection.insertOne(mongoDoc, { session })
|
|
427
|
+
: await collection.insertOne(mongoDoc);
|
|
428
|
+
|
|
395
429
|
// Return API format document (convert _id to id)
|
|
396
430
|
return this.mapFromMongo({ ...mongoDoc, _id: result.insertedId });
|
|
397
431
|
}
|
|
@@ -401,19 +435,54 @@ export class MongoDriver implements Driver {
|
|
|
401
435
|
|
|
402
436
|
// Map API document (id) to MongoDB document (_id) for update data
|
|
403
437
|
// But we should not allow updating the _id field itself
|
|
404
|
-
const { id: _ignoredId, ...updateData } = data; // intentionally ignore id to prevent updating
|
|
438
|
+
const { id: _ignoredId, created_at: _ignoredCreatedAt, ...updateData } = data; // intentionally ignore id and created_at to prevent updating them
|
|
405
439
|
|
|
406
440
|
// Handle atomic operators if present
|
|
407
441
|
const isAtomic = Object.keys(updateData).some(k => k.startsWith('$'));
|
|
408
|
-
|
|
442
|
+
|
|
443
|
+
// Build the update object with updated_at timestamp
|
|
444
|
+
let update: UpdateFilter<any>;
|
|
445
|
+
if (isAtomic) {
|
|
446
|
+
// When using atomic operators, add updated_at to $set
|
|
447
|
+
// The spread is safe because id and created_at were already removed via destructuring
|
|
448
|
+
update = { ...updateData } as any;
|
|
449
|
+
if (!update.$set) {
|
|
450
|
+
update.$set = {};
|
|
451
|
+
}
|
|
452
|
+
update.$set.updated_at = new Date().toISOString();
|
|
453
|
+
} else {
|
|
454
|
+
// For regular updates, add updated_at to the data
|
|
455
|
+
updateData.updated_at = new Date().toISOString();
|
|
456
|
+
update = { $set: updateData };
|
|
457
|
+
}
|
|
409
458
|
|
|
410
|
-
|
|
411
|
-
|
|
459
|
+
// Use findOneAndUpdate to return the updated document
|
|
460
|
+
const mongoOptions: FindOneAndUpdateOptions = { returnDocument: 'after' };
|
|
461
|
+
// Support both 'transaction' and 'session' option keys for compatibility
|
|
462
|
+
const session = options?.transaction || options?.session;
|
|
463
|
+
if (session) {
|
|
464
|
+
mongoOptions.session = session;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const result = await collection.findOneAndUpdate(
|
|
468
|
+
{ _id: this.normalizeId(id) },
|
|
469
|
+
update,
|
|
470
|
+
mongoOptions
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
// Return API format document (convert _id to id)
|
|
474
|
+
// findOneAndUpdate returns an object with a 'value' property containing the document
|
|
475
|
+
return this.mapFromMongo(result?.value);
|
|
412
476
|
}
|
|
413
477
|
|
|
414
478
|
async delete(objectName: string, id: string | number, options?: any) {
|
|
415
479
|
const collection = await this.getCollection(objectName);
|
|
416
|
-
|
|
480
|
+
// Pass session for transactional operations only if it exists
|
|
481
|
+
// Support both 'transaction' and 'session' option keys for compatibility
|
|
482
|
+
const session = options?.transaction || options?.session;
|
|
483
|
+
const result = session
|
|
484
|
+
? await collection.deleteOne({ _id: this.normalizeId(id) }, { session })
|
|
485
|
+
: await collection.deleteOne({ _id: this.normalizeId(id) });
|
|
417
486
|
return result.deletedCount;
|
|
418
487
|
}
|
|
419
488
|
|
|
@@ -441,7 +510,11 @@ export class MongoDriver implements Driver {
|
|
|
441
510
|
}
|
|
442
511
|
return mongoDoc;
|
|
443
512
|
});
|
|
444
|
-
|
|
513
|
+
// Support both 'transaction' and 'session' option keys for compatibility
|
|
514
|
+
const session = options?.transaction || options?.session;
|
|
515
|
+
const result = session
|
|
516
|
+
? await collection.insertMany(mongoDocs, { session })
|
|
517
|
+
: await collection.insertMany(mongoDocs);
|
|
445
518
|
// Return API format (convert _id to id)
|
|
446
519
|
return Object.values(result.insertedIds).map(id => ({ id }));
|
|
447
520
|
}
|
|
@@ -456,15 +529,23 @@ export class MongoDriver implements Driver {
|
|
|
456
529
|
const isAtomic = Object.keys(updateData).some(k => k.startsWith('$'));
|
|
457
530
|
const update = isAtomic ? updateData : { $set: updateData };
|
|
458
531
|
|
|
459
|
-
|
|
460
|
-
|
|
532
|
+
// Support both 'transaction' and 'session' option keys for compatibility
|
|
533
|
+
const session = options?.transaction || options?.session;
|
|
534
|
+
const result = session
|
|
535
|
+
? await collection.updateMany(filter, update, { session })
|
|
536
|
+
: await collection.updateMany(filter, update);
|
|
537
|
+
return { modifiedCount: result.modifiedCount };
|
|
461
538
|
}
|
|
462
539
|
|
|
463
540
|
async deleteMany(objectName: string, filters: any, options?: any): Promise<any> {
|
|
464
541
|
const collection = await this.getCollection(objectName);
|
|
465
542
|
const filter = this.mapFilters(filters);
|
|
466
|
-
|
|
467
|
-
|
|
543
|
+
// Support both 'transaction' and 'session' option keys for compatibility
|
|
544
|
+
const session = options?.transaction || options?.session;
|
|
545
|
+
const result = session
|
|
546
|
+
? await collection.deleteMany(filter, { session })
|
|
547
|
+
: await collection.deleteMany(filter);
|
|
548
|
+
return { deletedCount: result.deletedCount };
|
|
468
549
|
}
|
|
469
550
|
|
|
470
551
|
async aggregate(objectName: string, pipeline: any[], options?: any): Promise<any[]> {
|
|
@@ -474,12 +555,237 @@ export class MongoDriver implements Driver {
|
|
|
474
555
|
return this.mapFromMongoArray(results);
|
|
475
556
|
}
|
|
476
557
|
|
|
558
|
+
/**
|
|
559
|
+
* Get distinct values for a field
|
|
560
|
+
* @param objectName - The collection name
|
|
561
|
+
* @param field - The field to get distinct values from
|
|
562
|
+
* @param filters - Optional filters to apply
|
|
563
|
+
* @param options - Optional query options
|
|
564
|
+
* @returns Array of distinct values
|
|
565
|
+
*/
|
|
566
|
+
async distinct(objectName: string, field: string, filters?: any, options?: any): Promise<any[]> {
|
|
567
|
+
const collection = await this.getCollection(objectName);
|
|
568
|
+
|
|
569
|
+
// Convert ObjectQL filters to MongoDB query format
|
|
570
|
+
const filter = filters ? this.mapFilters(filters) : {};
|
|
571
|
+
|
|
572
|
+
// Use MongoDB's native distinct method
|
|
573
|
+
const results = await collection.distinct(field, filter);
|
|
574
|
+
|
|
575
|
+
return results;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Find one document and update it atomically
|
|
580
|
+
* @param objectName - The collection name
|
|
581
|
+
* @param filters - Query filters to find the document
|
|
582
|
+
* @param update - Update operations to apply
|
|
583
|
+
* @param options - Optional query options (e.g., returnDocument, upsert)
|
|
584
|
+
* @returns The updated document
|
|
585
|
+
*
|
|
586
|
+
* @example
|
|
587
|
+
* // Find and update with returnDocument: 'after' to get the updated doc
|
|
588
|
+
* const updated = await driver.findOneAndUpdate('users',
|
|
589
|
+
* { email: 'user@example.com' },
|
|
590
|
+
* { $set: { status: 'active' } },
|
|
591
|
+
* { returnDocument: 'after' }
|
|
592
|
+
* );
|
|
593
|
+
*/
|
|
594
|
+
async findOneAndUpdate(objectName: string, filters: any, update: any, options?: any): Promise<any> {
|
|
595
|
+
const collection = await this.getCollection(objectName);
|
|
596
|
+
|
|
597
|
+
// Convert ObjectQL filters to MongoDB query format
|
|
598
|
+
const filter = this.mapFilters(filters);
|
|
599
|
+
|
|
600
|
+
// MongoDB findOneAndUpdate options
|
|
601
|
+
const mongoOptions: any = {
|
|
602
|
+
returnDocument: options?.returnDocument || 'after', // 'before' or 'after'
|
|
603
|
+
upsert: options?.upsert || false
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
// Execute the atomic find and update
|
|
607
|
+
const result = await collection.findOneAndUpdate(filter, update, mongoOptions);
|
|
608
|
+
|
|
609
|
+
// Map MongoDB document to API format (convert _id to id)
|
|
610
|
+
// MongoDB driver v5+ returns { value: document, ok: number }
|
|
611
|
+
// Older versions (v4) return the document directly
|
|
612
|
+
// We handle both for backward compatibility
|
|
613
|
+
const doc = result?.value !== undefined ? result.value : result;
|
|
614
|
+
return doc ? this.mapFromMongo(doc) : null;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// ========== Transaction Support ==========
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Begin a new transaction session
|
|
621
|
+
*
|
|
622
|
+
* @returns MongoDB ClientSession that can be used for transactional operations
|
|
623
|
+
*
|
|
624
|
+
* @example
|
|
625
|
+
* const session = await driver.beginTransaction();
|
|
626
|
+
* try {
|
|
627
|
+
* await driver.create('users', { name: 'Alice' }, { session });
|
|
628
|
+
* await driver.create('orders', { userId: 'alice' }, { session });
|
|
629
|
+
* await driver.commitTransaction(session);
|
|
630
|
+
* } catch (error) {
|
|
631
|
+
* await driver.rollbackTransaction(session);
|
|
632
|
+
* throw error;
|
|
633
|
+
* }
|
|
634
|
+
*/
|
|
635
|
+
async beginTransaction(): Promise<any> {
|
|
636
|
+
await this.connected;
|
|
637
|
+
const session = this.client.startSession();
|
|
638
|
+
session.startTransaction();
|
|
639
|
+
return session;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Commit a transaction
|
|
644
|
+
*
|
|
645
|
+
* @param transaction - MongoDB ClientSession returned by beginTransaction()
|
|
646
|
+
*
|
|
647
|
+
* @example
|
|
648
|
+
* const session = await driver.beginTransaction();
|
|
649
|
+
* // ... perform operations with { session } in options
|
|
650
|
+
* await driver.commitTransaction(session);
|
|
651
|
+
*/
|
|
652
|
+
async commitTransaction(transaction: any): Promise<void> {
|
|
653
|
+
if (!transaction || typeof transaction.commitTransaction !== 'function') {
|
|
654
|
+
throw new Error('Invalid transaction object. Must be a MongoDB ClientSession.');
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
try {
|
|
658
|
+
await transaction.commitTransaction();
|
|
659
|
+
} finally {
|
|
660
|
+
await transaction.endSession();
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Rollback a transaction
|
|
666
|
+
*
|
|
667
|
+
* @param transaction - MongoDB ClientSession returned by beginTransaction()
|
|
668
|
+
*
|
|
669
|
+
* @example
|
|
670
|
+
* const session = await driver.beginTransaction();
|
|
671
|
+
* try {
|
|
672
|
+
* // ... perform operations
|
|
673
|
+
* await driver.commitTransaction(session);
|
|
674
|
+
* } catch (error) {
|
|
675
|
+
* await driver.rollbackTransaction(session);
|
|
676
|
+
* }
|
|
677
|
+
*/
|
|
678
|
+
async rollbackTransaction(transaction: any): Promise<void> {
|
|
679
|
+
if (!transaction || typeof transaction.abortTransaction !== 'function') {
|
|
680
|
+
throw new Error('Invalid transaction object. Must be a MongoDB ClientSession.');
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
try {
|
|
684
|
+
await transaction.abortTransaction();
|
|
685
|
+
} finally {
|
|
686
|
+
await transaction.endSession();
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
477
690
|
async disconnect() {
|
|
691
|
+
// Close all active change streams
|
|
692
|
+
for (const [streamId, stream] of this.changeStreams.entries()) {
|
|
693
|
+
await stream.close();
|
|
694
|
+
}
|
|
695
|
+
this.changeStreams.clear();
|
|
696
|
+
|
|
697
|
+
// Close the MongoDB client
|
|
478
698
|
if (this.client) {
|
|
479
699
|
await this.client.close();
|
|
480
700
|
}
|
|
481
701
|
}
|
|
482
702
|
|
|
703
|
+
/**
|
|
704
|
+
* Watch for changes in a collection using MongoDB Change Streams
|
|
705
|
+
* @param objectName - The collection name to watch
|
|
706
|
+
* @param handler - Callback function to handle change events
|
|
707
|
+
* @param options - Optional change stream configuration
|
|
708
|
+
* @returns Stream ID that can be used to close the stream later
|
|
709
|
+
*
|
|
710
|
+
* @example
|
|
711
|
+
* const streamId = await driver.watch('users', async (change) => {
|
|
712
|
+
* console.log('Change detected:', change.operationType);
|
|
713
|
+
* if (change.operationType === 'insert') {
|
|
714
|
+
* console.log('New document:', change.fullDocument);
|
|
715
|
+
* }
|
|
716
|
+
* }, {
|
|
717
|
+
* operationTypes: ['insert', 'update'],
|
|
718
|
+
* fullDocument: 'updateLookup'
|
|
719
|
+
* });
|
|
720
|
+
*
|
|
721
|
+
* // Later, to stop watching:
|
|
722
|
+
* await driver.unwatchChangeStream(streamId);
|
|
723
|
+
*/
|
|
724
|
+
async watch(objectName: string, handler: ChangeStreamHandler, options?: ChangeStreamOptions): Promise<string> {
|
|
725
|
+
const collection = await this.getCollection(objectName);
|
|
726
|
+
|
|
727
|
+
// Build change stream pipeline
|
|
728
|
+
const pipeline: any[] = options?.pipeline || [];
|
|
729
|
+
|
|
730
|
+
// Add operation type filter if specified
|
|
731
|
+
if (options?.operationTypes && options.operationTypes.length > 0) {
|
|
732
|
+
pipeline.unshift({
|
|
733
|
+
$match: {
|
|
734
|
+
operationType: { $in: options.operationTypes }
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Configure change stream options
|
|
740
|
+
const streamOptions: any = {};
|
|
741
|
+
if (options?.fullDocument) {
|
|
742
|
+
streamOptions.fullDocument = options.fullDocument;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// Create the change stream
|
|
746
|
+
const changeStream = collection.watch(pipeline, streamOptions);
|
|
747
|
+
|
|
748
|
+
// Generate unique stream ID
|
|
749
|
+
const streamId = `${objectName}_${Date.now()}_${Math.random().toString(36).substring(7)}`;
|
|
750
|
+
|
|
751
|
+
// Store the stream
|
|
752
|
+
this.changeStreams.set(streamId, changeStream);
|
|
753
|
+
|
|
754
|
+
// Handle change events
|
|
755
|
+
changeStream.on('change', async (change) => {
|
|
756
|
+
try {
|
|
757
|
+
await handler(change);
|
|
758
|
+
} catch (error) {
|
|
759
|
+
console.error(`[MongoDriver] Error in change stream handler for ${objectName}:`, error);
|
|
760
|
+
}
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
changeStream.on('error', (error) => {
|
|
764
|
+
console.error(`[MongoDriver] Change stream error for ${objectName}:`, error);
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
return streamId;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Stop watching a change stream
|
|
772
|
+
* @param streamId - The stream ID returned by watch()
|
|
773
|
+
*/
|
|
774
|
+
async unwatchChangeStream(streamId: string): Promise<void> {
|
|
775
|
+
const stream = this.changeStreams.get(streamId);
|
|
776
|
+
if (stream) {
|
|
777
|
+
await stream.close();
|
|
778
|
+
this.changeStreams.delete(streamId);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* Get all active change stream IDs
|
|
784
|
+
*/
|
|
785
|
+
getActiveChangeStreams(): string[] {
|
|
786
|
+
return Array.from(this.changeStreams.keys());
|
|
787
|
+
}
|
|
788
|
+
|
|
483
789
|
/**
|
|
484
790
|
* Execute a query using QueryAST (DriverInterface v4.0 method)
|
|
485
791
|
*
|
package/test/index.test.ts
CHANGED
|
@@ -22,6 +22,7 @@ const mockCollection = {
|
|
|
22
22
|
insertedCount: 2
|
|
23
23
|
}),
|
|
24
24
|
updateOne: jest.fn().mockResolvedValue({ modifiedCount: 1 }),
|
|
25
|
+
findOneAndUpdate: jest.fn().mockResolvedValue({ value: { _id: '123', name: 'Updated' } }),
|
|
25
26
|
deleteOne: jest.fn().mockResolvedValue({ deletedCount: 1 }),
|
|
26
27
|
countDocuments: jest.fn().mockResolvedValue(10)
|
|
27
28
|
};
|
|
@@ -437,20 +438,19 @@ describe('MongoDriver', () => {
|
|
|
437
438
|
data: { name: 'Updated User' }
|
|
438
439
|
};
|
|
439
440
|
|
|
440
|
-
mockCollection.
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
name: 'Updated User'
|
|
441
|
+
mockCollection.findOneAndUpdate.mockResolvedValue({
|
|
442
|
+
value: {
|
|
443
|
+
_id: '123',
|
|
444
|
+
name: 'Updated User',
|
|
445
|
+
updated_at: new Date().toISOString()
|
|
446
|
+
}
|
|
447
447
|
});
|
|
448
448
|
|
|
449
449
|
const result = await driver.executeCommand(command);
|
|
450
450
|
|
|
451
451
|
expect(result.success).toBe(true);
|
|
452
452
|
expect(result.affected).toBe(1);
|
|
453
|
-
expect(mockCollection.
|
|
453
|
+
expect(mockCollection.findOneAndUpdate).toHaveBeenCalled();
|
|
454
454
|
});
|
|
455
455
|
|
|
456
456
|
it('should execute delete command', async () => {
|
|
@@ -504,11 +504,13 @@ describe('MongoDriver', () => {
|
|
|
504
504
|
]
|
|
505
505
|
};
|
|
506
506
|
|
|
507
|
-
mockCollection.
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
507
|
+
mockCollection.findOneAndUpdate.mockResolvedValue({
|
|
508
|
+
value: {
|
|
509
|
+
_id: '1',
|
|
510
|
+
name: 'Updated 1',
|
|
511
|
+
updated_at: new Date().toISOString()
|
|
512
|
+
}
|
|
513
|
+
});
|
|
512
514
|
|
|
513
515
|
const result = await driver.executeCommand(command);
|
|
514
516
|
|