Skip to content

Commit 88ea568

Browse files
mshanemciowillhoit
andauthored
Sm/metadata-preview (#506)
* refactor: swap custom sleep for kit/sleep * test: update nonSupportedTypes * refactor: use test1 coverage report schema * feat: metadata type preview * feat: ci and slack integration * refactor: cleaner code * style: pr feedback * test: more registry validations * fix: slack message formatting Co-authored-by: Eric Willhoit <[email protected]>
1 parent 47e4250 commit 88ea568

File tree

9 files changed

+215
-34
lines changed

9 files changed

+215
-34
lines changed

.circleci/config.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,16 @@ jobs:
1313
- run: yarn build
1414
- run: yarn test:registry
1515

16+
registry-check-preview:
17+
description: Checks registry against next version of metadataCoverageReport, reporting results to slack
18+
docker:
19+
- image: node:lts
20+
steps:
21+
- checkout
22+
- run: yarn install
23+
- run: yarn build
24+
- run: yarn metadata:preview
25+
1626
external-nut:
1727
description: Runs NUTs from other (external) repos by cloning them. Substitutes a dependency for the current pull request. For example, you're testing a PR to a library and want to test a plugin in another repo that uses the library.
1828

@@ -102,8 +112,22 @@ jobs:
102112
echo "Environment Variables:"
103113
env
104114
NODE_OPTIONS=--max-old-space-size=8192 yarn test:nuts
115+
105116
workflows:
106117
version: 2
118+
registry-check-preview:
119+
triggers:
120+
- schedule:
121+
# weekly on Monday morning
122+
cron: 30 3 * * 1
123+
filters:
124+
branches:
125+
only:
126+
- main
127+
jobs:
128+
- registry-check-preview:
129+
# required for slack webhook
130+
context: salesforce-cli
107131
registry-check:
108132
triggers:
109133
- schedule:

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
"local:install": "./scripts/localInstall.js install",
9292
"local:link": "./scripts/localInstall.js link",
9393
"local:unlink": "./scripts/localInstall.js unlink",
94+
"metadata:preview": "npx ts-node scripts/update-registry/preview.ts",
9495
"prepack": "sf-prepack",
9596
"pretest": "sf-compile-test",
9697
"repl": "node --inspect ./scripts/repl.js",

scripts/update-registry/preview.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { CoverageObject } from '../../src/registry/types';
2+
import got from 'got';
3+
import { getMissingTypes } from '../../test/utils/getMissingTypes';
4+
import { registry } from '../../src';
5+
6+
(async () => {
7+
const currentApiVersion = (
8+
JSON.parse((await got(`https://mdcoverage.secure.force.com/services/apexrest/report`)).body) as {
9+
versions: { selected: number };
10+
}
11+
).versions.selected;
12+
13+
const nextCoverage = JSON.parse(
14+
(await got(`https://na${currentApiVersion - 8}.test1.pc-rnd.salesforce.com/mdcoverage/api.jsp`)).body
15+
) as CoverageObject;
16+
17+
const missingTypes = getMissingTypes(nextCoverage, registry).map((type) => type[0]);
18+
19+
console.log(`There are ${missingTypes.length} new types for v${nextCoverage.apiVersion} not in the registry.`);
20+
21+
const typesByFeature = new Map<string, string[]>();
22+
missingTypes.map((t) => {
23+
const featureLabel = nextCoverage.types[t].orgShapes.developer.features?.join(' & ') ?? 'NO FEATURE REQUIRED';
24+
typesByFeature.set(featureLabel, [...(typesByFeature.get(featureLabel) ?? []), t]);
25+
});
26+
console.log(typesByFeature);
27+
const formattedTypes = Array.from(typesByFeature, ([feature, types]) => `*${feature}*\n - ${types.join('\n - ')}`);
28+
29+
const json = {
30+
blocks: [
31+
{
32+
type: 'header',
33+
text: {
34+
type: 'plain_text',
35+
text: `v${nextCoverage.apiVersion} Metadata Preview`,
36+
},
37+
},
38+
{
39+
type: 'section',
40+
text: {
41+
type: 'plain_text',
42+
text: `There are ${missingTypes.length} new types not in the registry, organized by required features (if any).`,
43+
},
44+
},
45+
{
46+
type: 'section',
47+
text: {
48+
type: 'mrkdwn',
49+
text: formattedTypes.join('\n\n'),
50+
},
51+
},
52+
],
53+
};
54+
try {
55+
await got.post(process.env.DEFAULT_SLACK_WEBHOOK, {
56+
json,
57+
});
58+
} catch (e) {
59+
console.error(e);
60+
}
61+
})();

scripts/update-registry/update2.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ const getMissingTypesAsDescribeResult = (missingTypes: [string, CoverageObjectTy
103103

104104
const updateProjectScratchDef = (missingTypes: [string, CoverageObjectType][]) => {
105105
const scratchDefSummary = deepmerge.all(
106-
[{}].concat(missingTypes.map(([key, missingType]) => JSON.parse(missingType.scratchDefinitions.developer)))
106+
[{}].concat(missingTypes.map(([key, missingType]) => missingType.orgShapes.developer))
107107
) as {
108108
features: string[];
109109
};

src/client/deployStrategies/containerDeploy.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
77
import { readFileSync } from 'graceful-fs';
8+
import { sleep } from '@salesforce/kit';
9+
810
import { deployTypes } from '../toolingApi';
911
import { DeployError } from '../../errors';
1012
import {
@@ -106,7 +108,7 @@ export class ContainerDeploy extends BaseDeploy {
106108
let containerStatus: ContainerAsyncRequest;
107109
do {
108110
if (count > 0) {
109-
await this.sleep(100);
111+
await sleep(100);
110112
}
111113
containerStatus = (await this.connection.tooling.retrieve(
112114
ContainerDeploy.CONTAINER_ASYNC_REQUEST,
@@ -117,10 +119,6 @@ export class ContainerDeploy extends BaseDeploy {
117119
return containerStatus;
118120
}
119121

120-
private sleep(ms: number): Promise<number> {
121-
return new Promise((resolve) => setTimeout(resolve, ms));
122-
}
123-
124122
private buildSourceDeployResult(containerRequest: ContainerAsyncRequest): SourceDeployResult {
125123
const componentDeployment: ComponentDeployment = {
126124
component: this.component,

src/registry/nonSupportedTypes.ts

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@ export const metadataTypes = [
2626

2727
// things that don't show up in describe so far
2828
'PicklistValue', // only existed in v37, so it's hard to describe!
29-
'FieldRestrictionRule', // not in describe for devorg. ScratchDef might need feature 'EMPLOYEEEXPERIENCE' but it doesn't say that
30-
'AppointmentSchedulingPolicy', // not in describe?
3129
'AppointmentAssignmentPolicy', // not in describe?
3230
'WorkflowFlowAction', // not in describe
3331
'AdvAcctForecastDimSource', // not in describe
@@ -40,21 +38,15 @@ export const metadataTypes = [
4038
];
4139

4240
export const hasUnsupportedFeatures = (type: CoverageObjectType): boolean => {
43-
if (!type.scratchDefinitions?.developer) {
41+
if (!type.orgShapes?.developer) {
4442
return true;
4543
}
46-
const scratchDef = JSON.parse(type.scratchDefinitions.developer) as {
47-
features?: string[];
48-
settings?: {
49-
[key: string]: unknown;
50-
};
51-
};
44+
5245
if (
53-
scratchDef.features &&
54-
scratchDef.features.length > 0 &&
55-
features.some((feature) => scratchDef.features.includes(feature))
46+
type.orgShapes.developer.features?.length &&
47+
features.some((feature) => type.orgShapes?.developer.features.includes(feature))
5648
) {
5749
return true;
5850
}
59-
return scratchDef.settings && settings.some((setting) => scratchDef.settings[setting]);
51+
return type.orgShapes?.developer.settings && settings.some((setting) => type.orgShapes?.developer.settings[setting]);
6052
};

src/registry/types.ts

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -178,20 +178,23 @@ export const enum TransformerStrategy {
178178
NonDecomposed = 'nonDecomposed',
179179
}
180180

181+
interface Channel {
182+
exposed: boolean;
183+
}
181184
/**
182185
* Subset of an item from the Metadata Coverage Report
183186
*/
184187
export interface CoverageObjectType {
185-
scratchDefinitions: {
186-
professional: string;
187-
group: string;
188-
enterprise: string;
189-
developer: string;
188+
orgShapes: {
189+
developer: {
190+
features?: string[];
191+
settings?: Record<string, Record<string, unknown>>;
192+
};
190193
};
191194
channels: {
192-
metadataApi: boolean;
193-
sourceTracking: boolean;
194-
toolingApi: boolean;
195+
metadataApi: Channel;
196+
sourceTracking: Channel;
197+
toolingApi: Channel;
195198
};
196199
}
197200

@@ -202,9 +205,7 @@ export interface CoverageObject {
202205
types: {
203206
[key: string]: CoverageObjectType;
204207
};
205-
versions: {
206-
selected: number;
207-
max: number;
208-
min: number;
209-
};
208+
// only exists on the test1 instances flavor of coverage report
209+
apiVersion: number;
210+
release: string;
210211
}

test/registry/registryValidation.test.ts

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import { expect } from 'chai';
88
import { MetadataRegistry } from '../../src';
99
import { registry as defaultRegistry } from '../../src/registry/registry';
10-
import { MetadataType } from '../../src/registry/types';
10+
import { MetadataType, TransformerStrategy, DecompositionStrategy } from '../../src/registry/types';
1111

1212
describe('Registry Validation', () => {
1313
const registry = defaultRegistry as MetadataRegistry;
@@ -194,4 +194,108 @@ describe('Registry Validation', () => {
194194
});
195195
});
196196
});
197+
198+
describe('top level required properties', () => {
199+
describe('all have names and directoryName', () => {
200+
Object.entries(registry.types).forEach(([key, type]) => {
201+
it(`${type.id} has a name`, () => {
202+
expect(type.name).to.be.a('string');
203+
});
204+
it(`${type.id} has a directoryName`, () => {
205+
expect(type.directoryName).to.be.a('string');
206+
});
207+
});
208+
});
209+
});
210+
211+
describe('valid strategies', () => {
212+
const typesWithStrategies = Object.values(registry.types).filter((type) => type.strategies);
213+
214+
// there isn't an enum for this in Types, to the known are hardcoded here
215+
describe('valid, known adapters', () => {
216+
typesWithStrategies.forEach((type) => {
217+
it(`${type.id} has a valid adapter`, () => {
218+
expect(['default', 'mixedContent', 'bundle', 'matchingContentFile', 'decomposed']).includes(
219+
type.strategies.adapter
220+
);
221+
});
222+
});
223+
});
224+
225+
describe('adapter = matchingContentFile => no other strategy properties', () => {
226+
typesWithStrategies
227+
.filter((t) => t.strategies.adapter === 'matchingContentFile')
228+
.forEach((type) => {
229+
it(`${type.id} has no other strategy properties`, () => {
230+
expect(type.strategies.decomposition).to.be.undefined;
231+
expect(type.strategies.recomposition).to.be.undefined;
232+
expect(type.strategies.transformer).to.be.undefined;
233+
});
234+
});
235+
});
236+
237+
describe('adapter = bundle => no other strategy properties', () => {
238+
typesWithStrategies
239+
.filter((t) => t.strategies.adapter === 'bundle')
240+
.forEach((type) => {
241+
it(`${type.id} has no other strategy properties`, () => {
242+
expect(type.strategies.decomposition).to.be.undefined;
243+
expect(type.strategies.recomposition).to.be.undefined;
244+
expect(type.strategies.transformer).to.be.undefined;
245+
});
246+
});
247+
});
248+
249+
describe('adapter = decomposed => has transformer and decomposition props', () => {
250+
typesWithStrategies
251+
.filter((t) => t.strategies.adapter === 'decomposed')
252+
.forEach((type) => {
253+
it(`${type.id} has expected properties`, () => {
254+
expect(type.strategies.decomposition).to.be.a('string');
255+
expect(
256+
[DecompositionStrategy.FolderPerType.valueOf(), DecompositionStrategy.TopLevel.valueOf()].includes(
257+
type.strategies.decomposition
258+
)
259+
).to.be.true;
260+
expect(type.strategies.transformer).to.be.a('string');
261+
expect(
262+
[
263+
TransformerStrategy.Standard.valueOf(),
264+
TransformerStrategy.Decomposed.valueOf(),
265+
TransformerStrategy.StaticResource.valueOf(),
266+
TransformerStrategy.NonDecomposed.valueOf(),
267+
].includes(type.strategies.transformer)
268+
).to.be.true;
269+
expect(type.strategies.recomposition).to.be.undefined;
270+
});
271+
});
272+
});
273+
it('no standard types specified in registry', () => {
274+
expect(typesWithStrategies.filter((t) => t.strategies.transformer === 'standard')).to.have.length(0);
275+
});
276+
describe('adapter = mixedContent => has no decomposition/recomposition props', () => {
277+
typesWithStrategies
278+
.filter((t) => t.strategies.adapter === 'mixedContent')
279+
.forEach((type) => {
280+
it(`${type.id} has expected properties`, () => {
281+
expect(type.strategies.decomposition).to.be.undefined;
282+
expect(type.strategies.recomposition).to.be.undefined;
283+
type.strategies.transformer
284+
? expect(type.strategies.transformer).to.be.a('string')
285+
: expect(type.strategies.transformer).to.be.undefined;
286+
});
287+
});
288+
});
289+
});
290+
291+
describe('folders', () => {
292+
const folderTypes = Object.values(registry.types).filter((type) => type.inFolder);
293+
294+
folderTypes.forEach((type) => {
295+
it(`${type.name} has a valid folderType in the registry`, () => {
296+
expect(type.folderType).to.not.be.undefined;
297+
expect(registry.types[type.folderType]).to.be.an('object');
298+
});
299+
});
300+
});
197301
});

test/utils/getMissingTypes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export const getMissingTypes = (
1414
): Array<[string, CoverageObjectType]> => {
1515
const metadataApiTypesFromCoverage = Object.entries(metadataCoverage.types).filter(
1616
([key, value]) =>
17-
value.channels.metadataApi && // if it's not in the mdapi, we don't worry about the registry
17+
value.channels.metadataApi.exposed && // if it's not in the mdapi, we don't worry about the registry
1818
!metadataTypes.includes(key) && // types we should ignore, see the imported file for explanations
1919
!key.endsWith('Settings') && // individual settings shouldn't be in the registry
2020
!hasUnsupportedFeatures(value) // we don't support these types

0 commit comments

Comments
 (0)