Skip to content

Commit 8096081

Browse files
committed
feat: more fields from sourceMember
1 parent 8971d13 commit 8096081

File tree

4 files changed

+374
-156
lines changed

4 files changed

+374
-156
lines changed

src/shared/functions.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,15 @@ import { ensureArray } from '@salesforce/kit';
2121
import { RemoteChangeElement, ChangeResult, ChangeResultWithNameAndType, RemoteSyncInput } from './types';
2222
import { ensureNameAndType } from './remoteChangeIgnoring';
2323

24+
const keySplit = '###';
25+
const legacyKeySplit = '__';
2426
export const getMetadataKey = (metadataType: string, metadataName: string): string =>
25-
`${metadataType}__${metadataName}`;
27+
`${metadataType}${keySplit}${metadataName}`;
28+
export const getMetadataTypeFromKey = (key: string): string => decodeURIComponent(key.split(keySplit)[0]);
29+
export const getMetadataNameFromKey = (key: string): string => decodeURIComponent(key.split(keySplit)[1]);
30+
export const getMetadataTypeFromLegacyKey = (key: string): string => key.split(legacyKeySplit)[0];
31+
export const getMetadataNameFromLegacyKey = (key: string): string =>
32+
decodeURIComponent(key.split(legacyKeySplit).slice(1).join(legacyKeySplit));
2633

2734
export const getKeyFromObject = (element: RemoteChangeElement | ChangeResult): string => {
2835
if (element.type && element.name) {

src/shared/remoteSourceTrackingService.ts

+78-28
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,33 @@ import {
1919
SourceMember,
2020
RemoteSyncInput,
2121
SourceMemberPollingEvent,
22+
MemberRevisionLegacy,
2223
} from './types';
2324
import { getMetadataKeyFromFileResponse, mappingsForSourceMemberTypesToMetadataType } from './metadataKeys';
24-
import { getMetadataKey } from './functions';
25+
import { getMetadataKey, getMetadataNameFromLegacyKey, getMetadataTypeFromLegacyKey } from './functions';
2526
import { calculateExpectedSourceMembers } from './expectedSourceMembers';
2627

2728
/** represents the contents of the config file stored in 'maxRevision.json' */
2829
export type Contents = {
30+
fileVersion?: number;
2931
serverMaxRevisionCounter: number;
3032
sourceMembers: Record<string, MemberRevision>;
3133
};
32-
type MemberRevisionMapEntry = [string, MemberRevision];
34+
3335
type PinoLogger = ReturnType<(typeof Logger)['getRawRootLogger']>;
3436

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>;
3646

47+
const FILENAME = 'maxRevision.json';
48+
const CURRENT_FILE_VERSION = 1;
3749
/*
3850
* after some results have returned, how many times should we poll for missing sourcemembers
3951
* even when there is a longer timeout remaining (because the deployment is very large)
@@ -161,10 +173,13 @@ export class RemoteSourceTrackingService {
161173
} else if (doesNotMatchServer(revision)) {
162174
quietLogger(
163175
`Syncing ${metadataKey} revision from ${revision.lastRetrievedFromServer ?? 'null'} to ${
164-
revision.serverRevisionCounter
176+
revision.RevisionCounter
165177
}`
166178
);
167-
this.setMemberRevision(metadataKey, { ...revision, lastRetrievedFromServer: revision.serverRevisionCounter });
179+
this.setMemberRevision(metadataKey, {
180+
...revision,
181+
lastRetrievedFromServer: revision.RevisionCounter,
182+
});
168183
}
169184
});
170185

@@ -215,7 +230,7 @@ export class RemoteSourceTrackingService {
215230

216231
// Look for any changed that haven't been synced. I.e, the lastRetrievedFromServer
217232
// does not match the serverRevisionCounter.
218-
const returnElements = Array.from(this.sourceMembers.entries())
233+
const returnElements = Array.from(this.sourceMembers.values())
219234
.filter(revisionDoesNotMatch)
220235
.map(revisionToRemoteChangeElement);
221236

@@ -392,17 +407,17 @@ ${formatSourceMemberWarnings(outstandingSourceMembers)}`
392407
// try accessing the sourceMembers object at the index of the change's name
393408
// if it exists, we'll update the fields - if it doesn't, we'll create and insert it
394409
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) {
402417
// We are already tracking this element so we'll update it
403418
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;
406421
} else {
407422
// We are not yet tracking it so we'll insert a new record
408423
quietLogger(`Inserting ${key} with RevisionCounter: ${change.RevisionCounter}${sync ? ' and syncing' : ''}`);
@@ -411,11 +426,11 @@ ${formatSourceMemberWarnings(outstandingSourceMembers)}`
411426
// If we are syncing changes then we need to update the lastRetrievedFromServer field to
412427
// match the RevisionCounter from the SourceMember.
413428
if (sync) {
414-
sourceMember.lastRetrievedFromServer = change.RevisionCounter;
429+
sourceMemberFromTracking.lastRetrievedFromServer = change.RevisionCounter;
415430
}
416431

417432
// Update the state with the latest SourceMember data
418-
this.setMemberRevision(key, sourceMember);
433+
this.setMemberRevision(key, sourceMemberFromTracking);
419434
});
420435

421436
await this.write();
@@ -458,7 +473,7 @@ ${formatSourceMemberWarnings(outstandingSourceMembers)}`
458473
const matchingKey = sourceMembers.get(key)
459474
? key
460475
: getDecodedKeyIfSourceMembersHas({ sourceMembers, key, logger: this.logger });
461-
this.sourceMembers.set(matchingKey, sourceMember);
476+
this.sourceMembers.set(matchingKey, { ...sourceMember, MemberName: decodeURIComponent(sourceMember.MemberName) });
462477
}
463478

464479
private async querySourceMembersFrom({
@@ -478,7 +493,7 @@ ${formatSourceMemberWarnings(outstandingSourceMembers)}`
478493
}
479494

480495
// 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}`;
482497
this.logger[quiet ? 'silent' : 'debug'](`Query: ${query}`);
483498
const queryResult = await queryFn(this.org.getConnection(), query);
484499
this.queryCache.set(rev, queryResult);
@@ -491,9 +506,10 @@ ${formatSourceMemberWarnings(outstandingSourceMembers)}`
491506
await lockResult.writeAndUnlock(
492507
JSON.stringify(
493508
{
509+
fileVersion: CURRENT_FILE_VERSION,
494510
serverMaxRevisionCounter: this.serverMaxRevisionCounter,
495511
sourceMembers: Object.fromEntries(this.sourceMembers),
496-
},
512+
} satisfies Contents,
497513
null,
498514
4
499515
)
@@ -518,10 +534,13 @@ export const remoteChangeElementToChangeResult = (rce: RemoteChangeElement): Cha
518534
origin: 'remote', // we know they're remote
519535
});
520536

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,
525544
});
526545

527546
/**
@@ -560,7 +579,16 @@ const getFilePath = (orgId: string): string => path.join('.sf', 'orgs', orgId, F
560579
const readFileContents = async (filePath: string): Promise<Contents | Record<string, never>> => {
561580
try {
562581
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);
564592
} catch (e) {
565593
Logger.childFromRoot('remoteSourceTrackingService:readFileContents').debug(
566594
`Error reading or parsing file file at ${filePath}. Will treat as an empty file.`,
@@ -588,9 +616,31 @@ export const calculateTimeout =
588616
return Duration.seconds(pollingTimeout);
589617
};
590618

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+
591641
/** exported only for spy/mock */
592642
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}`;
594644
return queryFn(conn, query);
595645
};
596646

@@ -616,10 +666,10 @@ const formatSourceMemberWarnings = (outstandingSourceMembers: Map<string, Remote
616666
.join(EOL);
617667
};
618668

619-
const revisionDoesNotMatch = ([, member]: MemberRevisionMapEntry): boolean => doesNotMatchServer(member);
669+
const revisionDoesNotMatch = (member: MemberRevision): boolean => doesNotMatchServer(member);
620670

621671
const doesNotMatchServer = (member: MemberRevision): boolean =>
622-
member.serverRevisionCounter !== member.lastRetrievedFromServer;
672+
member.RevisionCounter !== member.lastRetrievedFromServer;
623673

624674
/** 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 */
625675
const sourceMemberCorrections = (sourceMember: SourceMember): SourceMember => {

src/shared/types.ts

+13-4
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ export type RemoteChangeElement = {
3131
type: string;
3232
deleted?: boolean;
3333
modified?: boolean;
34+
changedBy: string;
35+
revisionCounter: number;
3436
};
3537

3638
/**
@@ -42,19 +44,26 @@ export type ChangeResult = Partial<RemoteChangeElement> & {
4244
ignored?: boolean;
4345
};
4446

45-
export type MemberRevision = {
47+
export type MemberRevision = SourceMember & {
48+
/** the last revision retrieved. Used for detecting changes*/
49+
lastRetrievedFromServer?: number;
50+
};
51+
52+
/** @deprecated replaced by the new MemberRevision */
53+
export type MemberRevisionLegacy = {
54+
memberType: string;
4655
serverRevisionCounter: number;
4756
lastRetrievedFromServer: number | null;
48-
memberType: string;
4957
isNameObsolete: boolean;
5058
};
51-
5259
export type SourceMember = {
5360
MemberType: string;
5461
MemberName: string;
5562
IsNameObsolete: boolean;
63+
IsNewMember: boolean;
5664
RevisionCounter: number;
57-
ignored?: boolean;
65+
MemberIdOrName: string;
66+
ChangedBy: string;
5867
};
5968

6069
export type ConflictResponse = {

0 commit comments

Comments
 (0)