@@ -19,21 +19,33 @@ import {
19
19
SourceMember ,
20
20
RemoteSyncInput ,
21
21
SourceMemberPollingEvent ,
22
+ MemberRevisionLegacy ,
22
23
} from './types' ;
23
24
import { getMetadataKeyFromFileResponse , mappingsForSourceMemberTypesToMetadataType } from './metadataKeys' ;
24
- import { getMetadataKey } from './functions' ;
25
+ import { getMetadataKey , getMetadataNameFromLegacyKey , getMetadataTypeFromLegacyKey } from './functions' ;
25
26
import { calculateExpectedSourceMembers } from './expectedSourceMembers' ;
26
27
27
28
/** represents the contents of the config file stored in 'maxRevision.json' */
28
29
export type Contents = {
30
+ fileVersion ?: number ;
29
31
serverMaxRevisionCounter : number ;
30
32
sourceMembers : Record < string , MemberRevision > ;
31
33
} ;
32
- type MemberRevisionMapEntry = [ string , MemberRevision ] ;
34
+
33
35
type PinoLogger = ReturnType < ( typeof Logger ) [ 'getRawRootLogger' ] > ;
34
36
35
- const FILENAME = 'maxRevision.json' ;
37
+ const SOURCE_MEMBER_FIELDS = [
38
+ 'MemberIdOrName' ,
39
+ 'MemberType' ,
40
+ 'MemberName' ,
41
+ 'IsNameObsolete' ,
42
+ 'RevisionCounter' ,
43
+ 'IsNewMember' ,
44
+ 'ChangedBy' ,
45
+ ] satisfies Array < keyof SourceMember > ;
36
46
47
+ const FILENAME = 'maxRevision.json' ;
48
+ const CURRENT_FILE_VERSION = 1 ;
37
49
/*
38
50
* after some results have returned, how many times should we poll for missing sourcemembers
39
51
* even when there is a longer timeout remaining (because the deployment is very large)
@@ -161,10 +173,13 @@ export class RemoteSourceTrackingService {
161
173
} else if ( doesNotMatchServer ( revision ) ) {
162
174
quietLogger (
163
175
`Syncing ${ metadataKey } revision from ${ revision . lastRetrievedFromServer ?? 'null' } to ${
164
- revision . serverRevisionCounter
176
+ revision . RevisionCounter
165
177
} `
166
178
) ;
167
- this . setMemberRevision ( metadataKey , { ...revision , lastRetrievedFromServer : revision . serverRevisionCounter } ) ;
179
+ this . setMemberRevision ( metadataKey , {
180
+ ...revision ,
181
+ lastRetrievedFromServer : revision . RevisionCounter ,
182
+ } ) ;
168
183
}
169
184
} ) ;
170
185
@@ -215,7 +230,7 @@ export class RemoteSourceTrackingService {
215
230
216
231
// Look for any changed that haven't been synced. I.e, the lastRetrievedFromServer
217
232
// does not match the serverRevisionCounter.
218
- const returnElements = Array . from ( this . sourceMembers . entries ( ) )
233
+ const returnElements = Array . from ( this . sourceMembers . values ( ) )
219
234
. filter ( revisionDoesNotMatch )
220
235
. map ( revisionToRemoteChangeElement ) ;
221
236
@@ -392,17 +407,17 @@ ${formatSourceMemberWarnings(outstandingSourceMembers)}`
392
407
// try accessing the sourceMembers object at the index of the change's name
393
408
// if it exists, we'll update the fields - if it doesn't, we'll create and insert it
394
409
const key = getMetadataKey ( change . MemberType , change . MemberName ) ;
395
- const sourceMember = this . getSourceMember ( key ) ?? {
396
- serverRevisionCounter : change . RevisionCounter ,
397
- lastRetrievedFromServer : null ,
398
- memberType : change . MemberType ,
399
- isNameObsolete : change . IsNameObsolete ,
400
- } ;
401
- if ( sourceMember . lastRetrievedFromServer ) {
410
+ const sourceMemberFromTracking =
411
+ this . getSourceMember ( key ) ??
412
+ ( {
413
+ ... change ,
414
+ lastRetrievedFromServer : undefined ,
415
+ } satisfies MemberRevision ) ;
416
+ if ( sourceMemberFromTracking . lastRetrievedFromServer ) {
402
417
// We are already tracking this element so we'll update it
403
418
quietLogger ( `Updating ${ key } to RevisionCounter: ${ change . RevisionCounter } ${ sync ? ' and syncing' : '' } ` ) ;
404
- sourceMember . serverRevisionCounter = change . RevisionCounter ;
405
- sourceMember . isNameObsolete = change . IsNameObsolete ;
419
+ sourceMemberFromTracking . RevisionCounter = change . RevisionCounter ;
420
+ sourceMemberFromTracking . IsNameObsolete = change . IsNameObsolete ;
406
421
} else {
407
422
// We are not yet tracking it so we'll insert a new record
408
423
quietLogger ( `Inserting ${ key } with RevisionCounter: ${ change . RevisionCounter } ${ sync ? ' and syncing' : '' } ` ) ;
@@ -411,11 +426,11 @@ ${formatSourceMemberWarnings(outstandingSourceMembers)}`
411
426
// If we are syncing changes then we need to update the lastRetrievedFromServer field to
412
427
// match the RevisionCounter from the SourceMember.
413
428
if ( sync ) {
414
- sourceMember . lastRetrievedFromServer = change . RevisionCounter ;
429
+ sourceMemberFromTracking . lastRetrievedFromServer = change . RevisionCounter ;
415
430
}
416
431
417
432
// Update the state with the latest SourceMember data
418
- this . setMemberRevision ( key , sourceMember ) ;
433
+ this . setMemberRevision ( key , sourceMemberFromTracking ) ;
419
434
} ) ;
420
435
421
436
await this . write ( ) ;
@@ -458,7 +473,7 @@ ${formatSourceMemberWarnings(outstandingSourceMembers)}`
458
473
const matchingKey = sourceMembers . get ( key )
459
474
? key
460
475
: getDecodedKeyIfSourceMembersHas ( { sourceMembers, key, logger : this . logger } ) ;
461
- this . sourceMembers . set ( matchingKey , sourceMember ) ;
476
+ this . sourceMembers . set ( matchingKey , { ... sourceMember , MemberName : decodeURIComponent ( sourceMember . MemberName ) } ) ;
462
477
}
463
478
464
479
private async querySourceMembersFrom ( {
@@ -478,7 +493,7 @@ ${formatSourceMemberWarnings(outstandingSourceMembers)}`
478
493
}
479
494
480
495
// because `serverMaxRevisionCounter` is always updated, we need to select > to catch the most recent change
481
- const query = `SELECT MemberType, MemberName, IsNameObsolete, RevisionCounter FROM SourceMember WHERE RevisionCounter > ${ rev } ` ;
496
+ const query = `SELECT ${ SOURCE_MEMBER_FIELDS . join ( ', ' ) } FROM SourceMember WHERE RevisionCounter > ${ rev } ` ;
482
497
this . logger [ quiet ? 'silent' : 'debug' ] ( `Query: ${ query } ` ) ;
483
498
const queryResult = await queryFn ( this . org . getConnection ( ) , query ) ;
484
499
this . queryCache . set ( rev , queryResult ) ;
@@ -491,9 +506,10 @@ ${formatSourceMemberWarnings(outstandingSourceMembers)}`
491
506
await lockResult . writeAndUnlock (
492
507
JSON . stringify (
493
508
{
509
+ fileVersion : CURRENT_FILE_VERSION ,
494
510
serverMaxRevisionCounter : this . serverMaxRevisionCounter ,
495
511
sourceMembers : Object . fromEntries ( this . sourceMembers ) ,
496
- } ,
512
+ } satisfies Contents ,
497
513
null ,
498
514
4
499
515
)
@@ -518,10 +534,13 @@ export const remoteChangeElementToChangeResult = (rce: RemoteChangeElement): Cha
518
534
origin : 'remote' , // we know they're remote
519
535
} ) ;
520
536
521
- const revisionToRemoteChangeElement = ( [ memberKey , memberRevision ] : MemberRevisionMapEntry ) : RemoteChangeElement => ( {
522
- type : memberRevision . memberType ,
523
- name : memberKey . replace ( `${ memberRevision . memberType } __` , '' ) ,
524
- deleted : memberRevision . isNameObsolete ,
537
+ const revisionToRemoteChangeElement = ( memberRevision : MemberRevision ) : RemoteChangeElement => ( {
538
+ type : memberRevision . MemberType ,
539
+ name : memberRevision . MemberName ,
540
+ deleted : memberRevision . IsNameObsolete ,
541
+ modified : memberRevision . IsNewMember === false ,
542
+ revisionCounter : memberRevision . RevisionCounter ,
543
+ changedBy : memberRevision . ChangedBy ,
525
544
} ) ;
526
545
527
546
/**
@@ -560,7 +579,16 @@ const getFilePath = (orgId: string): string => path.join('.sf', 'orgs', orgId, F
560
579
const readFileContents = async ( filePath : string ) : Promise < Contents | Record < string , never > > => {
561
580
try {
562
581
const contents = await fs . promises . readFile ( filePath , 'utf8' ) ;
563
- return parseJsonMap < Contents > ( contents , filePath ) ;
582
+ const parsedContents = parseJsonMap < Contents > ( contents , filePath ) ;
583
+ if ( parsedContents . fileVersion === CURRENT_FILE_VERSION ) {
584
+ return parsedContents ;
585
+ }
586
+ Logger . childFromRoot ( 'remoteSourceTrackingService:readFileContents' ) . debug (
587
+ `File version mismatch. Expected ${ CURRENT_FILE_VERSION } , found ${
588
+ parsedContents . fileVersion ?? 'undefined'
589
+ } . Upgrading file contents. Some expected data may be missing`
590
+ ) ;
591
+ return upgradeFileContents ( parsedContents ) ;
564
592
} catch ( e ) {
565
593
Logger . childFromRoot ( 'remoteSourceTrackingService:readFileContents' ) . debug (
566
594
`Error reading or parsing file file at ${ filePath } . Will treat as an empty file.` ,
@@ -588,9 +616,31 @@ export const calculateTimeout =
588
616
return Duration . seconds ( pollingTimeout ) ;
589
617
} ;
590
618
619
+ /** exported for unit testing */
620
+ export const upgradeFileContents = ( contents : Contents ) : Contents => ( {
621
+ fileVersion : 1 ,
622
+ serverMaxRevisionCounter : contents . serverMaxRevisionCounter ,
623
+ // @ts -expect-error the old file didn't store the IsNewMember field or any indication of whether the member was add/modified
624
+ sourceMembers : Object . fromEntries (
625
+ // it's the old version
626
+ Object . entries ( contents . sourceMembers as unknown as Record < string , MemberRevisionLegacy > ) . map ( ( [ key , value ] ) => [
627
+ getMetadataKey ( getMetadataTypeFromLegacyKey ( key ) , getMetadataNameFromLegacyKey ( key ) ) ,
628
+ {
629
+ MemberName : getMetadataNameFromLegacyKey ( key ) ,
630
+ MemberType : value . memberType ,
631
+ IsNameObsolete : value . isNameObsolete ,
632
+ RevisionCounter : value . serverRevisionCounter ,
633
+ lastRetrievedFromServer : value . lastRetrievedFromServer ?? undefined ,
634
+ ChangedBy : 'unknown' ,
635
+ MemberIdOrName : 'unknown' ,
636
+ } ,
637
+ ] )
638
+ ) ,
639
+ } ) ;
640
+
591
641
/** exported only for spy/mock */
592
642
export const querySourceMembersTo = async ( conn : Connection , toRevision : number ) : Promise < SourceMember [ ] > => {
593
- const query = `SELECT MemberType, MemberName, IsNameObsolete, RevisionCounter FROM SourceMember WHERE RevisionCounter <= ${ toRevision } ` ;
643
+ const query = `SELECT ${ SOURCE_MEMBER_FIELDS . join ( ', ' ) } FROM SourceMember WHERE RevisionCounter <= ${ toRevision } ` ;
594
644
return queryFn ( conn , query ) ;
595
645
} ;
596
646
@@ -616,10 +666,10 @@ const formatSourceMemberWarnings = (outstandingSourceMembers: Map<string, Remote
616
666
. join ( EOL ) ;
617
667
} ;
618
668
619
- const revisionDoesNotMatch = ( [ , member ] : MemberRevisionMapEntry ) : boolean => doesNotMatchServer ( member ) ;
669
+ const revisionDoesNotMatch = ( member : MemberRevision ) : boolean => doesNotMatchServer ( member ) ;
620
670
621
671
const doesNotMatchServer = ( member : MemberRevision ) : boolean =>
622
- member . serverRevisionCounter !== member . lastRetrievedFromServer ;
672
+ member . RevisionCounter !== member . lastRetrievedFromServer ;
623
673
624
674
/** A series of workarounds for server-side bugs. Each bug should be filed against a team, with a WI, so we know when these are fixed and can be removed */
625
675
const sourceMemberCorrections = ( sourceMember : SourceMember ) : SourceMember => {
0 commit comments