Skip to content

Commit

Permalink
feat: warn about legacy suffix use (#1298)
Browse files Browse the repository at this point in the history
* feat: warn about legacy suffix use

* fix: more support for legacy suffix

* test: snapshot for bigObject indexe

* test: extra ut for legacy suffix

* test: handle potentially undefined childType

* chore: resolve a TODO

* fix: legacy suffix warning in the correct place

* feat: also check legacy suffix for strict folder matching
  • Loading branch information
mshanemc committed Apr 30, 2024
1 parent 721d4b4 commit 6ad87d2
Show file tree
Hide file tree
Showing 15 changed files with 264 additions and 25 deletions.
1 change: 1 addition & 0 deletions src/convert/convertContext/recompositionFinalizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ const ensureStateValueWithParent = (
);
};

/** throw if the child has no parent component */
const ensureMetadataComponentWithParent = (
child: MetadataComponent
): child is SourceComponent & { parent: SourceComponent } => {
Expand Down
6 changes: 5 additions & 1 deletion src/convert/transformers/defaultMetadataTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import { basename, dirname, join } from 'node:path';
import { Messages } from '@salesforce/core';
import { Messages } from '@salesforce/core/messages';
import { Lifecycle } from '@salesforce/core/lifecycle';
import { SourcePath } from '../../common/types';
import { META_XML_SUFFIX } from '../../common/constants';
import { SfdxFileFormat, WriteInfo } from '../types';
Expand Down Expand Up @@ -118,6 +119,9 @@ const getXmlDestination = (
}
}
if (legacySuffix && suffix && xmlDestination.includes(legacySuffix)) {
void Lifecycle.getInstance().emitWarning(
`The ${component.type.name} component ${component.fullName} uses the legacy suffix ${legacySuffix}. This suffix is deprecated and will be removed in a future release.`
);
xmlDestination = xmlDestination.replace(legacySuffix, suffix);
}
return xmlDestination;
Expand Down
30 changes: 24 additions & 6 deletions src/resolve/metadataResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -385,20 +385,38 @@ const resolveTypeFromStrictFolder =
.filter(folderTypeFilter(fsPath))
.find(
(type) =>
// any of the following 3 options is considered a good match
isMixedContentOrBundle(type) || suffixMatches(type, fsPath) || childSuffixMatches(type, fsPath)
// any of the following options is considered a good match
isMixedContentOrBundle(type) ||
suffixMatches(type, fsPath) ||
childSuffixMatches(type, fsPath) ||
legacySuffixMatches(type, fsPath)
);
};

/** the type has children and the file suffix (in source format) matches a child type suffix of the type we think it is */
const childSuffixMatches = (type: MetadataType, fsPath: string): boolean =>
Object.values(type.children?.types ?? {})
.map((childType) => `${childType.suffix}${META_XML_SUFFIX}`)
.some((s) => fsPath.endsWith(s));
Object.values(type.children?.types ?? {}).some(
(childType) => suffixMatches(childType, fsPath) || legacySuffixMatches(childType, fsPath)
);

/** the file suffix (in source or mdapi format) matches the type suffix we think it is */
const suffixMatches = (type: MetadataType, fsPath: string): boolean =>
typeof type.suffix === 'string' && [type.suffix, `${type.suffix}${META_XML_SUFFIX}`].some((s) => fsPath.endsWith(s));
typeof type.suffix === 'string' &&
(fsPath.endsWith(type.suffix) || fsPath.endsWith(appendMetaXmlSuffix(type.suffix)));

const legacySuffixMatches = (type: MetadataType, fsPath: string): boolean => {
if (
typeof type.legacySuffix === 'string' &&
(fsPath.endsWith(type.legacySuffix) || fsPath.endsWith(appendMetaXmlSuffix(type.legacySuffix)))
) {
void Lifecycle.getInstance().emitWarning(
`The ${type.name} component at ${fsPath} uses the legacy suffix ${type.legacySuffix}. This suffix is deprecated and will be removed in a future release.`
);
return true;
}
return false;
};
const appendMetaXmlSuffix = (suffix: string): string => `${suffix}${META_XML_SUFFIX}`;

const isMixedContentOrBundle = (type: MetadataType): boolean =>
typeof type.strategies?.adapter === 'string' && ['mixedContent', 'bundle'].includes(type.strategies.adapter);
Expand Down
17 changes: 12 additions & 5 deletions src/resolve/sourceComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import { join, dirname } from 'node:path';
import { Messages, SfError } from '@salesforce/core';
import { SfError } from '@salesforce/core/sfError';
import { Messages } from '@salesforce/core/messages';
import { Lifecycle } from '@salesforce/core/lifecycle';

import { XMLParser, XMLValidator } from 'fast-xml-parser';
import { get, getString, JsonMap } from '@salesforce/ts-types';
import { ensureArray } from '@salesforce/kit';
Expand Down Expand Up @@ -310,14 +313,18 @@ export class SourceComponent implements MetadataComponent {
const children: SourceComponent[] = [];
for (const fsPath of this.walk(dirPath)) {
const childXml = parseMetadataXml(fsPath);
const fileIsRootXml = childXml?.suffix === this.type.suffix;
const fileIsRootXml = childXml?.suffix === this.type.suffix || childXml?.suffix === this.type.legacySuffix;
if (childXml && !fileIsRootXml && this.type.children && childXml.suffix) {
// TODO: Log warning if missing child type definition
const childTypeId = this.type.children?.suffixes[childXml.suffix];
const childSuffix = this.type.children.types[childTypeId]?.suffix;
const childType = this.type.children.types[childTypeId];
if (!childTypeId || !childType) {
void Lifecycle.getInstance().emitWarning(
`${fsPath}: Expected a child type for ${childXml.suffix} in ${this.type.name} but none was found.`
);
}
const childComponent = new SourceComponent(
{
name: childSuffix ? baseWithoutSuffixes(fsPath, childSuffix) : baseName(fsPath),
name: childType?.suffix ? baseWithoutSuffixes(fsPath, childType) : baseName(fsPath),
type: this.type.children.types[childTypeId],
xml: fsPath,
parent: this,
Expand Down
15 changes: 8 additions & 7 deletions src/utils/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,19 @@ export function baseName(fsPath: SourcePath): string {

/**
* the above baseName function doesn't handle components whose names have a `.` in them.
* this will handle that, but requires you to specify the expected suffix from the mdType.
* this will handle that, but requires you to specify the mdType to check suffixes for.
*
* @param fsPath The path to evaluate
*/
export function baseWithoutSuffixes(fsPath: SourcePath, suffix: string): string {
return basename(fsPath)
.replace(META_XML_SUFFIX, '')
.split('.')
.filter((part) => part !== suffix)
.join('.');
export function baseWithoutSuffixes(fsPath: SourcePath, mdType: MetadataType): string {
return basename(fsPath).replace(META_XML_SUFFIX, '').split('.').filter(stringIsNotSuffix(mdType)).join('.');
}

const stringIsNotSuffix =
(type: MetadataType) =>
(part: string): boolean =>
part !== type.suffix && (!type.legacySuffix || part !== type.legacySuffix);

/**
* Get the name of file path extension. Different from path.extname in that it
* does not include the '.' in the extension name. Returns an empty string if
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomObject xmlns="http://soap.sforce.com/2006/04/metadata">
<deploymentStatus>Deployed</deploymentStatus>
<description>Big Object representation of Logger data, used as an alternative to the platform event LogEntryEvent__e, as well as a way to archive Logger data stored in Log__c, LogEntry__, and LogEntryTag__c</description>
<label>Log Entry Archive</label>
<pluralLabel>Log Entry Archives</pluralLabel>
<fields>
<fullName>Timestamp__c</fullName>
<businessStatus>Active</businessStatus>
<complianceGroup>None</complianceGroup>
<externalId>false</externalId>
<label>Timestamp</label>
<required>true</required>
<securityClassification>Confidential</securityClassification>
<type>DateTime</type>
</fields>
<fields>
<fullName>TransactionEntryNumber__c</fullName>
<businessStatus>Active</businessStatus>
<complianceGroup>None</complianceGroup>
<description>The sequential number of this log entry within the transaction</description>
<externalId>false</externalId>
<label>Entry #</label>
<precision>10</precision>
<required>true</required>
<scale>0</scale>
<securityClassification>Confidential</securityClassification>
<type>Number</type>
<unique>false</unique>
</fields>
<fields>
<fullName>TransactionId__c</fullName>
<businessStatus>Active</businessStatus>
<complianceGroup>None</complianceGroup>
<externalId>false</externalId>
<label>Transaction ID</label>
<length>36</length>
<required>true</required>
<securityClassification>Confidential</securityClassification>
<type>Text</type>
<unique>false</unique>
</fields>
<indexes>
<fullName>LogEntryArchiveIndex</fullName>
<fields>
<name>Timestamp__c</name>
<sortDirection>DESC</sortDirection>
</fields>
<fields>
<name>TransactionId__c</name>
<sortDirection>ASC</sortDirection>
</fields>
<fields>
<name>TransactionEntryNumber__c</name>
<sortDirection>DESC</sortDirection>
</fields>
<label>Log Entry Archive Index</label>
</indexes>
</CustomObject>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
<types>
<members>LogEntryArchive__b.Timestamp__c</members>
<members>LogEntryArchive__b.TransactionEntryNumber__c</members>
<members>LogEntryArchive__b.TransactionId__c</members>
<name>CustomField</name>
</types>
<types>
<members>LogEntryArchive__b</members>
<name>CustomObject</name>
</types>
<types>
<members>LogEntryArchive__b.LogEntryArchiveIndex</members>
<name>Index</name>
</types>
<version>60.0</version>
</Package>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" ?>
<CustomObject xmlns="http://soap.sforce.com/2006/04/metadata">
<deploymentStatus>Deployed</deploymentStatus>
<description
>Big Object representation of Logger data, used as an alternative to the platform event LogEntryEvent__e, as well as a way to archive Logger data stored in Log__c, LogEntry__, and LogEntryTag__c</description>
<label>Log Entry Archive</label>
<pluralLabel>Log Entry Archives</pluralLabel>
</CustomObject>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8" ?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Timestamp__c</fullName>
<businessStatus>Active</businessStatus>
<complianceGroup>None</complianceGroup>
<externalId>false</externalId>
<label>Timestamp</label>
<required>true</required>
<securityClassification>Confidential</securityClassification>
<type>DateTime</type>
</CustomField>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" ?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>TransactionEntryNumber__c</fullName>
<businessStatus>Active</businessStatus>
<complianceGroup>None</complianceGroup>
<description>The sequential number of this log entry within the transaction</description>
<externalId>false</externalId>
<label>Entry #</label>
<precision>10</precision>
<required>true</required>
<scale>0</scale>
<securityClassification>Confidential</securityClassification>
<type>Number</type>
<unique>false</unique>
</CustomField>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8" ?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>TransactionId__c</fullName>
<businessStatus>Active</businessStatus>
<complianceGroup>None</complianceGroup>
<externalId>false</externalId>
<label>Transaction ID</label>
<length>36</length>
<required>true</required>
<securityClassification>Confidential</securityClassification>
<type>Text</type>
<unique>false</unique>
</CustomField>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Index xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>LogEntryArchiveIndex</fullName>
<fields>
<name>Timestamp__c</name>
<sortDirection>DESC</sortDirection>
</fields>
<fields>
<name>TransactionId__c</name>
<sortDirection>ASC</sortDirection>
</fields>
<fields>
<name>TransactionEntryNumber__c</name>
<sortDirection>DESC</sortDirection>
</fields>
<label>Log Entry Archive Index</label>
</Index>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"packageDirectories": [
{
"default": true,
"path": "force-app"
}
],
"sourceApiVersion": "59.0"
}
31 changes: 31 additions & 0 deletions test/snapshot/sampleProjects/legacySuffixSupport/snapshots.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright (c) 2023, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
import { MDAPI_OUT, fileSnap, sourceToMdapi } from '../../helper/conversions';

// we don't want failing tests outputting over each other
/* eslint-disable no-await-in-loop */

describe('legacy suffix support (.indexe for bigObject index)', () => {
const testDir = path.join('test', 'snapshot', 'sampleProjects', 'legacySuffixSupport');
let mdFiles: string[];

before(async () => {
mdFiles = await sourceToMdapi(testDir);
});

it('verify md files', async () => {
for (const file of mdFiles) {
await fileSnap(file, testDir);
}
});

after(async () => {
await Promise.all([fs.promises.rm(path.join(testDir, MDAPI_OUT), { recursive: true, force: true })]);
});
});
39 changes: 33 additions & 6 deletions test/utils/path.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { join } from 'node:path';
import { expect } from 'chai';
import { META_XML_SUFFIX } from '../../src/common';
import { parseMetadataXml, trimUntil, baseName, parseNestedFullName, baseWithoutSuffixes } from '../../src/utils';
import { MetadataType } from '../../src/registry/types';

describe('Path Utils', () => {
const root = join('path', 'to', 'whatever');
Expand All @@ -25,31 +26,57 @@ describe('Path Utils', () => {
});

describe('baseWithoutSuffixes', () => {
const mdTypeCommon: MetadataType = {
id: 'test',
name: 'Test',
directoryName: 'tests',
};
const mdType: MetadataType = {
...mdTypeCommon,
suffix: 'xyz',
};
const mdTypeLegacySuffix: MetadataType = {
...mdType,
suffix: 'xyz',
legacySuffix: 'xyzz',
};

it('Should strip specified suffixes from a file path with a dot', () => {
const path = join(root, 'a.ext.xyz');
expect(baseWithoutSuffixes(path, 'xyz')).to.equal('a.ext');
expect(baseWithoutSuffixes(path, mdType)).to.equal('a.ext');
});

it('Should strip specified suffixes from a file path with a dot and standard ending', () => {
const path = join(root, `a.ext.xyz${META_XML_SUFFIX}`);
expect(baseWithoutSuffixes(path, 'xyz')).to.equal('a.ext');
expect(baseWithoutSuffixes(path, mdType)).to.equal('a.ext');
});

it('Should handle paths with no suffixes', () => {
const path = join(root, 'a');
expect(baseWithoutSuffixes(path, 'ext')).to.equal('a');
expect(baseWithoutSuffixes(path, mdTypeCommon)).to.equal('a');
});

it('Should preserve non-matching suffixes', () => {
const path = join(root, 'a.xyz');
expect(baseWithoutSuffixes(path, 'ext')).to.equal('a.xyz');
expect(baseWithoutSuffixes(path, mdTypeCommon)).to.equal('a.xyz');
});

it('Should remove the standard suffix and a custom suffix', () => {
const path = join(root, `a.ext${META_XML_SUFFIX}`);
expect(baseWithoutSuffixes(path, 'ext')).to.equal('a');
const path = join(root, `a.xyz${META_XML_SUFFIX}`);
expect(baseWithoutSuffixes(path, mdType)).to.equal('a');
});

it('should remove a legacy suffix', () => {
const path = join(root, 'a.xyzz');
expect(baseWithoutSuffixes(path, mdTypeLegacySuffix)).to.equal('a');
});

it('should remove a legacy suffix with the standard meta', () => {
const path = join(root, `a.xyzz${META_XML_SUFFIX}`);
expect(baseWithoutSuffixes(path, mdTypeLegacySuffix)).to.equal('a');
});
});

describe('trimUntil', () => {
it('should return given path if part is not found', () => {
expect(trimUntil(root, 'test')).to.equal(root);
Expand Down

0 comments on commit 6ad87d2

Please sign in to comment.