diff --git a/README.md b/README.md
index 7f0ed99e..dd96e039 100644
--- a/README.md
+++ b/README.md
@@ -309,6 +309,15 @@ export default defineMarkdownConfig({
});
```
+You can also leverage the `exclude` property to indirectly modify things like custom metadata records you do
+not want included in the custom metadata type object documentation.
+
+```typescript
+//...
+exclude: ['**/*.md-meta.xml']
+//...
+```
+
### Excluding Tags from Appearing in the Documentation
Note: Only works for Markdown documentation.
diff --git a/examples/vitepress/docs/.vitepress/cache/deps/_metadata.json b/examples/vitepress/docs/.vitepress/cache/deps/_metadata.json
index 3c6ae1c5..e710f560 100644
--- a/examples/vitepress/docs/.vitepress/cache/deps/_metadata.json
+++ b/examples/vitepress/docs/.vitepress/cache/deps/_metadata.json
@@ -1,31 +1,31 @@
{
- "hash": "f2216e85",
+ "hash": "05eb2d4f",
"configHash": "7f7b0dad",
- "lockfileHash": "76121266",
- "browserHash": "441a8d6a",
+ "lockfileHash": "3a9c2374",
+ "browserHash": "a831d6e7",
"optimized": {
"vue": {
"src": "../../../../node_modules/vue/dist/vue.runtime.esm-bundler.js",
"file": "vue.js",
- "fileHash": "885cbaa9",
+ "fileHash": "1632d62a",
"needsInterop": false
},
"vitepress > @vue/devtools-api": {
"src": "../../../../node_modules/@vue/devtools-api/dist/index.js",
"file": "vitepress___@vue_devtools-api.js",
- "fileHash": "ff3ba36c",
+ "fileHash": "dc8fec00",
"needsInterop": false
},
"vitepress > @vueuse/core": {
"src": "../../../../node_modules/@vueuse/core/index.mjs",
"file": "vitepress___@vueuse_core.js",
- "fileHash": "b6cc6d79",
+ "fileHash": "3d02446b",
"needsInterop": false
},
"@theme/index": {
"src": "../../../../node_modules/vitepress/dist/client/theme-default/index.js",
"file": "@theme_index.js",
- "fileHash": "6b17bcd7",
+ "fileHash": "3d2d1de3",
"needsInterop": false
}
},
diff --git a/examples/vitepress/docs/.vitepress/sidebar.json b/examples/vitepress/docs/.vitepress/sidebar.json
index 6f2444a3..0fc24fcc 100644
--- a/examples/vitepress/docs/.vitepress/sidebar.json
+++ b/examples/vitepress/docs/.vitepress/sidebar.json
@@ -93,6 +93,10 @@
{
"text": "Speaker__c",
"link": "custom-objects/Speaker__c.md"
+ },
+ {
+ "text": "VisibleCMT__mdt",
+ "link": "custom-objects/VisibleCMT__mdt.md"
}
]
}
diff --git a/examples/vitepress/docs/changelog.md b/examples/vitepress/docs/changelog.md
index 3cc6f539..915a7aae 100644
--- a/examples/vitepress/docs/changelog.md
+++ b/examples/vitepress/docs/changelog.md
@@ -79,4 +79,12 @@ These custom fields have been added or removed.
### Contact
-- New Field: PhotoUrl__c. URL of the contact's photo
\ No newline at end of file
+- New Field: PhotoUrl__c. URL of the contact's photo
+
+## New or Removed Custom Metadata Type Records
+
+These custom metadata type records have been added or removed.
+
+### VisibleCMT__mdt
+
+- New Custom Metadata Record: Some_Record_1
\ No newline at end of file
diff --git a/examples/vitepress/docs/custom-objects/VisibleCMT__mdt.md b/examples/vitepress/docs/custom-objects/VisibleCMT__mdt.md
new file mode 100644
index 00000000..edc0ffa8
--- /dev/null
+++ b/examples/vitepress/docs/custom-objects/VisibleCMT__mdt.md
@@ -0,0 +1,29 @@
+---
+title: VisibleCMT__mdt
+---
+
+# VisibleCMT
+
+## API Name
+`VisibleCMT__mdt`
+
+## Fields
+### Field1
+**Required**
+
+**API Name**
+
+`apexdocs__Field1__c`
+
+**Type**
+
+*Text*
+
+## Records
+### Some Record 1
+
+`Protected`
+
+**API Name**
+
+`VisibleCMT.Some_Record_1`
\ No newline at end of file
diff --git a/examples/vitepress/docs/index.md b/examples/vitepress/docs/index.md
index 6d8ce3bb..ca295921 100644
--- a/examples/vitepress/docs/index.md
+++ b/examples/vitepress/docs/index.md
@@ -49,6 +49,8 @@ Represents a line item on a sales order.
Represents a speaker at an event.
+### [VisibleCMT__mdt](custom-objects/VisibleCMT__mdt)
+
## Miscellaneous
### [BaseClass](miscellaneous/BaseClass)
diff --git a/examples/vitepress/force-app/main/default/customMetadata/VisibleCMT.Some_Record_1.md-meta.xml b/examples/vitepress/force-app/main/default/customMetadata/VisibleCMT.Some_Record_1.md-meta.xml
new file mode 100644
index 00000000..13c352d4
--- /dev/null
+++ b/examples/vitepress/force-app/main/default/customMetadata/VisibleCMT.Some_Record_1.md-meta.xml
@@ -0,0 +1,9 @@
+
+
+
+ true
+
+ Field1__c
+ Sample Value
+
+
diff --git a/examples/vitepress/force-app/main/default/objects/SameNamespaceMDT__mdt/SameNamespaceMDT__mdt.object-meta.xml b/examples/vitepress/force-app/main/default/objects/SameNamespaceMDT__mdt/SameNamespaceMDT__mdt.object-meta.xml
new file mode 100644
index 00000000..a5d81fa8
--- /dev/null
+++ b/examples/vitepress/force-app/main/default/objects/SameNamespaceMDT__mdt/SameNamespaceMDT__mdt.object-meta.xml
@@ -0,0 +1,6 @@
+
+
+
+ SameNamespaceMDTs
+ Protected
+
diff --git a/examples/vitepress/force-app/main/default/objects/VisibleCMT__mdt/VisibleCMT__mdt.object-meta.xml b/examples/vitepress/force-app/main/default/objects/VisibleCMT__mdt/VisibleCMT__mdt.object-meta.xml
new file mode 100644
index 00000000..e4b702db
--- /dev/null
+++ b/examples/vitepress/force-app/main/default/objects/VisibleCMT__mdt/VisibleCMT__mdt.object-meta.xml
@@ -0,0 +1,6 @@
+
+
+
+ VisibleCMTs
+ Public
+
diff --git a/examples/vitepress/force-app/main/default/objects/VisibleCMT__mdt/fields/Field1__c.field-meta.xml b/examples/vitepress/force-app/main/default/objects/VisibleCMT__mdt/fields/Field1__c.field-meta.xml
new file mode 100644
index 00000000..8363be54
--- /dev/null
+++ b/examples/vitepress/force-app/main/default/objects/VisibleCMT__mdt/fields/Field1__c.field-meta.xml
@@ -0,0 +1,11 @@
+
+
+ Field1__c
+ false
+ DeveloperControlled
+
+ 255
+ true
+ Text
+ false
+
diff --git a/examples/vitepress/previous/force-app/main/default/objects/VisibleCMT__mdt/VisibleCMT__mdt.object-meta.xml b/examples/vitepress/previous/force-app/main/default/objects/VisibleCMT__mdt/VisibleCMT__mdt.object-meta.xml
new file mode 100644
index 00000000..e4b702db
--- /dev/null
+++ b/examples/vitepress/previous/force-app/main/default/objects/VisibleCMT__mdt/VisibleCMT__mdt.object-meta.xml
@@ -0,0 +1,6 @@
+
+
+
+ VisibleCMTs
+ Public
+
diff --git a/examples/vitepress/previous/force-app/main/default/objects/VisibleCMT__mdt/fields/Field1__c.field-meta.xml b/examples/vitepress/previous/force-app/main/default/objects/VisibleCMT__mdt/fields/Field1__c.field-meta.xml
new file mode 100644
index 00000000..8363be54
--- /dev/null
+++ b/examples/vitepress/previous/force-app/main/default/objects/VisibleCMT__mdt/fields/Field1__c.field-meta.xml
@@ -0,0 +1,11 @@
+
+
+ Field1__c
+ false
+ DeveloperControlled
+
+ 255
+ true
+ Text
+ false
+
diff --git a/package.json b/package.json
index d4c505b3..bbae05bd 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@cparra/apexdocs",
- "version": "3.7.3",
+ "version": "3.8.0",
"description": "Library with CLI capabilities to generate documentation for Salesforce Apex classes.",
"keywords": [
"apex",
diff --git a/src/application/Apexdocs.ts b/src/application/Apexdocs.ts
index e317b345..0369760a 100644
--- a/src/application/Apexdocs.ts
+++ b/src/application/Apexdocs.ts
@@ -6,7 +6,7 @@ import markdown from './generators/markdown';
import openApi from './generators/openapi';
import changelog from './generators/changelog';
-import { processFiles } from './source-code-file-reader';
+import { allComponentTypes, processFiles } from './source-code-file-reader';
import { DefaultFileSystem } from './file-system';
import { Logger } from '#utils/logger';
import {
@@ -52,10 +52,9 @@ async function processMarkdown(config: UserDefinedMarkdownConfig) {
return pipe(
E.tryCatch(
() =>
- readFiles(['ApexClass', 'CustomObject', 'CustomField'], { includeMetadata: config.includeMetadata })(
- config.sourceDir,
- config.exclude,
- ),
+ readFiles(allComponentTypes, {
+ includeMetadata: config.includeMetadata,
+ })(config.sourceDir, config.exclude),
(e) => new FileReadingError('An error occurred while reading files.', e),
),
TE.fromEither,
@@ -73,8 +72,8 @@ async function processOpenApi(config: UserDefinedOpenApiConfig, logger: Logger)
async function processChangeLog(config: UserDefinedChangelogConfig) {
function loadFiles(): [UnparsedSourceBundle[], UnparsedSourceBundle[]] {
return [
- readFiles(['ApexClass', 'CustomObject', 'CustomField'])(config.previousVersionDir, config.exclude),
- readFiles(['ApexClass', 'CustomObject', 'CustomField'])(config.currentVersionDir, config.exclude),
+ readFiles(allComponentTypes)(config.previousVersionDir, config.exclude),
+ readFiles(allComponentTypes)(config.currentVersionDir, config.exclude),
];
}
diff --git a/src/application/source-code-file-reader.ts b/src/application/source-code-file-reader.ts
index a2a81ae6..96bcaaf2 100644
--- a/src/application/source-code-file-reader.ts
+++ b/src/application/source-code-file-reader.ts
@@ -1,10 +1,16 @@
import { FileSystem } from './file-system';
-import { UnparsedApexBundle, UnparsedCustomFieldBundle, UnparsedCustomObjectBundle } from '../core/shared/types';
+import {
+ UnparsedApexBundle,
+ UnparsedCustomFieldBundle,
+ UnparsedCustomMetadataBundle,
+ UnparsedCustomObjectBundle,
+} from '../core/shared/types';
import { minimatch } from 'minimatch';
import { flow, pipe } from 'fp-ts/function';
import { apply } from '#utils/fp';
-type ComponentTypes = 'ApexClass' | 'CustomObject' | 'CustomField';
+export type ComponentTypes = 'ApexClass' | 'CustomObject' | 'CustomField' | 'CustomMetadata';
+export const allComponentTypes: ComponentTypes[] = ['ApexClass', 'CustomObject', 'CustomField', 'CustomMetadata'];
/**
* Simplified representation of a source component, with only
@@ -43,6 +49,14 @@ type CustomFieldSourceComponent = {
parentName: string;
};
+type CustomMetadataSourceComponent = {
+ type: 'CustomMetadata';
+ apiName: string;
+ name: string;
+ contentPath: string;
+ parentName: string;
+};
+
function getApexSourceComponents(
includeMetadata: boolean,
sourceComponents: SourceComponentAdapter[],
@@ -125,6 +139,41 @@ function toUnparsedCustomFieldBundle(
}));
}
+function getCustomMetadataSourceComponents(
+ sourceComponents: SourceComponentAdapter[],
+): CustomMetadataSourceComponent[] {
+ function getParentAndNamePair(component: SourceComponentAdapter): [string, string] {
+ // Custom metadata take the format [Namespace].[ParentName].[MetadataName], where namespace is optional.
+ // Here we split the strig and return the last 2 elements, representing the parent and the metadata name.
+ const [parentName, name] = component.name.split('.').slice(-2);
+ return [parentName, name];
+ }
+
+ return sourceComponents
+ .filter((component) => component.type.name === 'CustomMetadata')
+ .map((component) => ({
+ apiName: component.name,
+ name: getParentAndNamePair(component)[1],
+ type: 'CustomMetadata' as const,
+ contentPath: component.xml!,
+ parentName: getParentAndNamePair(component)[0],
+ }));
+}
+
+function toUnparsedCustomMetadataBundle(
+ fileSystem: FileSystem,
+ customMetadataSourceComponents: CustomMetadataSourceComponent[],
+): UnparsedCustomMetadataBundle[] {
+ return customMetadataSourceComponents.map((component) => ({
+ apiName: component.apiName,
+ type: 'custommetadata',
+ name: component.name,
+ filePath: component.contentPath,
+ content: fileSystem.readFile(component.contentPath),
+ parentName: component.parentName,
+ }));
+}
+
/**
* Reads from source code files and returns their raw body.
*/
@@ -137,7 +186,12 @@ export function processFiles(fileSystem: FileSystem) {
ComponentTypes,
(
components: SourceComponentAdapter[],
- ) => (UnparsedApexBundle | UnparsedCustomObjectBundle | UnparsedCustomFieldBundle)[]
+ ) => (
+ | UnparsedApexBundle
+ | UnparsedCustomObjectBundle
+ | UnparsedCustomFieldBundle
+ | UnparsedCustomMetadataBundle
+ )[]
> = {
ApexClass: flow(apply(getApexSourceComponents, options.includeMetadata), (apexSourceComponents) =>
toUnparsedApexBundle(fileSystem, apexSourceComponents),
@@ -148,6 +202,9 @@ export function processFiles(fileSystem: FileSystem) {
CustomField: flow(getCustomFieldSourceComponents, (customFieldSourceComponents) =>
toUnparsedCustomFieldBundle(fileSystem, customFieldSourceComponents),
),
+ CustomMetadata: flow(getCustomMetadataSourceComponents, (customMetadataSourceComponents) =>
+ toUnparsedCustomMetadataBundle(fileSystem, customMetadataSourceComponents),
+ ),
};
const convertersToUse = componentTypesToRetrieve.map((componentType) => converters[componentType]);
diff --git a/src/core/changelog/__test__/helpers/custom-field-metadata-builder.ts b/src/core/changelog/__test__/helpers/custom-field-metadata-builder.ts
new file mode 100644
index 00000000..6bdc8e5d
--- /dev/null
+++ b/src/core/changelog/__test__/helpers/custom-field-metadata-builder.ts
@@ -0,0 +1,28 @@
+import { CustomFieldMetadata } from '../../../reflection/sobject/reflect-custom-field-source';
+
+export default class CustomFieldMetadataBuilder {
+ name: string = 'MyField';
+ description: string | null = null;
+
+ withName(name: string): CustomFieldMetadataBuilder {
+ this.name = name;
+ return this;
+ }
+
+ withDescription(testDescription: string) {
+ this.description = testDescription;
+ return this;
+ }
+
+ build(): CustomFieldMetadata {
+ return {
+ type: 'Text',
+ type_name: 'customfield',
+ label: 'MyField',
+ name: this.name,
+ description: this.description,
+ parentName: 'MyObject',
+ required: false,
+ };
+ }
+}
diff --git a/src/core/changelog/__test__/helpers/custom-metadata-metadata-builder.ts b/src/core/changelog/__test__/helpers/custom-metadata-metadata-builder.ts
new file mode 100644
index 00000000..62241fb5
--- /dev/null
+++ b/src/core/changelog/__test__/helpers/custom-metadata-metadata-builder.ts
@@ -0,0 +1,22 @@
+import { CustomMetadataMetadata } from '../../../reflection/sobject/reflect-custom-metadata-source';
+
+export default class CustomMetadataMetadataBuilder {
+ parentName: string = 'MyObject';
+ name: string = 'FieldName__c';
+
+ withName(name: string): CustomMetadataMetadataBuilder {
+ this.name = name;
+ return this;
+ }
+
+ build(): CustomMetadataMetadata {
+ return {
+ type_name: 'custommetadata',
+ apiName: `${this.parentName}.${this.name}`,
+ protected: false,
+ label: 'MyMetadata',
+ name: this.name,
+ parentName: this.parentName,
+ };
+ }
+}
diff --git a/src/core/changelog/__test__/helpers/custom-object-metadata-builder.ts b/src/core/changelog/__test__/helpers/custom-object-metadata-builder.ts
new file mode 100644
index 00000000..62c6a61a
--- /dev/null
+++ b/src/core/changelog/__test__/helpers/custom-object-metadata-builder.ts
@@ -0,0 +1,32 @@
+import { CustomFieldMetadata } from '../../../reflection/sobject/reflect-custom-field-source';
+import { CustomObjectMetadata } from '../../../reflection/sobject/reflect-custom-object-sources';
+import { CustomMetadataMetadata } from '../../../reflection/sobject/reflect-custom-metadata-source';
+
+export default class CustomObjectMetadataBuilder {
+ label: string = 'MyObject';
+ fields: CustomFieldMetadata[] = [];
+ metadataRecords: CustomMetadataMetadata[] = [];
+
+ withField(field: CustomFieldMetadata): CustomObjectMetadataBuilder {
+ this.fields.push(field);
+ return this;
+ }
+
+ withMetadataRecord(metadataRecord: CustomMetadataMetadata): CustomObjectMetadataBuilder {
+ this.metadataRecords.push(metadataRecord);
+ return this;
+ }
+
+ build(): CustomObjectMetadata {
+ return {
+ type_name: 'customobject',
+ deploymentStatus: 'Deployed',
+ visibility: 'Public',
+ label: this.label,
+ name: 'MyObject',
+ description: null,
+ fields: this.fields,
+ metadataRecords: this.metadataRecords,
+ };
+ }
+}
diff --git a/src/core/changelog/__test__/processing-changelog.spec.ts b/src/core/changelog/__test__/processing-changelog.spec.ts
index ad8ea5a1..6eccbb8b 100644
--- a/src/core/changelog/__test__/processing-changelog.spec.ts
+++ b/src/core/changelog/__test__/processing-changelog.spec.ts
@@ -1,7 +1,8 @@
import { processChangelog } from '../process-changelog';
import { reflect, Type } from '@cparra/apex-reflection';
-import { CustomObjectMetadata } from '../../reflection/sobject/reflect-custom-object-sources';
-import { CustomFieldMetadata } from '../../reflection/sobject/reflect-custom-field-source';
+import CustomFieldMetadataBuilder from './helpers/custom-field-metadata-builder';
+import CustomObjectMetadataBuilder from './helpers/custom-object-metadata-builder';
+import CustomMetadataMetadataBuilder from './helpers/custom-metadata-metadata-builder';
function apexTypeFromRawString(raw: string): Type {
const result = reflect(raw);
@@ -12,55 +13,6 @@ function apexTypeFromRawString(raw: string): Type {
return result.typeMirror!;
}
-class CustomFieldMetadataBuilder {
- name: string = 'MyField';
- description: string | null = null;
-
- withName(name: string): CustomFieldMetadataBuilder {
- this.name = name;
- return this;
- }
-
- withDescription(testDescription: string) {
- this.description = testDescription;
- return this;
- }
-
- build(): CustomFieldMetadata {
- return {
- type: 'Text',
- type_name: 'customfield',
- label: 'MyField',
- name: this.name,
- description: this.description,
- parentName: 'MyObject',
- required: false,
- };
- }
-}
-
-class CustomObjectMetadataBuilder {
- label: string = 'MyObject';
- fields: CustomFieldMetadata[] = [];
-
- withField(field: CustomFieldMetadata): CustomObjectMetadataBuilder {
- this.fields.push(field);
- return this;
- }
-
- build(): CustomObjectMetadata {
- return {
- type_name: 'customobject',
- deploymentStatus: 'Deployed',
- visibility: 'Public',
- label: this.label,
- name: 'MyObject',
- description: null,
- fields: this.fields,
- };
- }
-}
-
describe('when generating a changelog', () => {
it('has no new types when both the old and new versions are empty', () => {
const oldVersion = { types: [] };
@@ -668,4 +620,71 @@ describe('when generating a changelog', () => {
]);
});
});
+
+ describe('with custom metadata records', () => {
+ it('does not list custom metadata records that are the same in both versions', () => {
+ // The record uniqueness is determined by its api name.
+
+ const oldCustomMetadata = new CustomMetadataMetadataBuilder().build();
+ const newCustomMetadata = new CustomMetadataMetadataBuilder().build();
+
+ const oldManifest = { types: [oldCustomMetadata] };
+ const newManifest = { types: [newCustomMetadata] };
+
+ const changeLog = processChangelog(oldManifest, newManifest);
+
+ expect(changeLog.customObjectModifications).toEqual([]);
+ });
+
+ it('lists new records of a custom object', () => {
+ const oldObject = new CustomObjectMetadataBuilder()
+ .withMetadataRecord(new CustomMetadataMetadataBuilder().build())
+ .build();
+ const newObject = new CustomObjectMetadataBuilder()
+ .withMetadataRecord(new CustomMetadataMetadataBuilder().build())
+ .withMetadataRecord(new CustomMetadataMetadataBuilder().withName('NewField__c').build())
+ .build();
+
+ const oldManifest = { types: [oldObject] };
+ const newManifest = { types: [newObject] };
+
+ const changeLog = processChangelog(oldManifest, newManifest);
+
+ expect(changeLog.customObjectModifications).toEqual([
+ {
+ typeName: newObject.name,
+ modifications: [
+ {
+ __typename: 'NewCustomMetadataRecord',
+ name: 'NewField__c',
+ },
+ ],
+ },
+ ]);
+ });
+
+ it('lists removed records of a custom object', () => {
+ const oldObject = new CustomObjectMetadataBuilder()
+ .withMetadataRecord(new CustomMetadataMetadataBuilder().withName('OldField__c').build())
+ .build();
+ const newObject = new CustomObjectMetadataBuilder().build();
+
+ const oldManifest = { types: [oldObject] };
+ const newManifest = { types: [newObject] };
+
+ const changeLog = processChangelog(oldManifest, newManifest);
+
+ expect(changeLog.customObjectModifications).toEqual([
+ {
+ typeName: oldObject.name,
+ modifications: [
+ {
+ __typename: 'RemovedCustomMetadataRecord',
+ name: 'OldField__c',
+ },
+ ],
+ },
+ ]);
+ });
+ });
});
diff --git a/src/core/changelog/generate-change-log.ts b/src/core/changelog/generate-change-log.ts
index a11d7daa..380f443b 100644
--- a/src/core/changelog/generate-change-log.ts
+++ b/src/core/changelog/generate-change-log.ts
@@ -18,13 +18,14 @@ import { HookError, ReflectionErrors } from '../errors/errors';
import { apply } from '#utils/fp';
import { filterScope } from '../reflection/apex/filter-scope';
import { isInSource, isSkip, passThroughHook, skip, toFrontmatterString } from '../shared/utils';
-import { reflectCustomFieldsAndObjects } from '../reflection/sobject/reflectCustomFieldsAndObjects';
+import { reflectCustomFieldsAndObjectsAndMetadataRecords } from '../reflection/sobject/reflectCustomFieldsAndObjectsAndMetadataRecords';
import { CustomObjectMetadata } from '../reflection/sobject/reflect-custom-object-sources';
import { Type } from '@cparra/apex-reflection';
-import { filterApexSourceFiles, filterCustomObjectsAndFields } from '#utils/source-bundle-utils';
+import { filterApexSourceFiles, filterCustomObjectsFieldsAndMetadataRecords } from '#utils/source-bundle-utils';
import { CustomFieldMetadata } from '../reflection/sobject/reflect-custom-field-source';
import { hookableTemplate } from '../markdown/templates/hookable';
import changelogToSourceChangelog from './helpers/changelog-to-source-changelog';
+import { CustomMetadataMetadata } from '../reflection/sobject/reflect-custom-metadata-source';
type Config = Omit;
@@ -70,7 +71,7 @@ function reflect(bundles: UnparsedSourceBundle[], config: Omit {
return pipe(
- reflectCustomFieldsAndObjects(filterCustomObjectsAndFields(bundles)),
+ reflectCustomFieldsAndObjectsAndMetadataRecords(filterCustomObjectsFieldsAndMetadataRecords(bundles)),
TE.map((parsedObjectFiles) => [...parsedApexFiles, ...parsedObjectFiles]),
);
}),
@@ -81,7 +82,10 @@ function toManifests({ oldVersion, newVersion }: { oldVersion: ParsedFile[]; new
function parsedFilesToManifest(parsedFiles: ParsedFile[]): VersionManifest {
return {
types: parsedFiles.reduce(
- (previousValue: (Type | CustomObjectMetadata | CustomFieldMetadata)[], parsedFile: ParsedFile) => {
+ (
+ previousValue: (Type | CustomObjectMetadata | CustomFieldMetadata | CustomMetadataMetadata)[],
+ parsedFile: ParsedFile,
+ ) => {
if (!isInSource(parsedFile.source) && parsedFile.type.type_name === 'customobject') {
// When we are dealing with a custom object that was not in the source (for extension fields), we return all
// of its fields.
@@ -89,7 +93,7 @@ function toManifests({ oldVersion, newVersion }: { oldVersion: ParsedFile[]; new
}
return [...previousValue, parsedFile.type];
},
- [] as (Type | CustomObjectMetadata | CustomFieldMetadata)[],
+ [] as (Type | CustomObjectMetadata | CustomFieldMetadata | CustomMetadataMetadata)[],
),
};
}
diff --git a/src/core/changelog/process-changelog.ts b/src/core/changelog/process-changelog.ts
index d689db40..64aeab26 100644
--- a/src/core/changelog/process-changelog.ts
+++ b/src/core/changelog/process-changelog.ts
@@ -3,9 +3,10 @@ import { pipe } from 'fp-ts/function';
import { areMethodsEqual } from './method-changes-checker';
import { CustomObjectMetadata } from '../reflection/sobject/reflect-custom-object-sources';
import { CustomFieldMetadata } from '../reflection/sobject/reflect-custom-field-source';
+import { CustomMetadataMetadata } from '../reflection/sobject/reflect-custom-metadata-source';
export type VersionManifest = {
- types: (Type | CustomObjectMetadata | CustomFieldMetadata)[];
+ types: (Type | CustomObjectMetadata | CustomFieldMetadata | CustomMetadataMetadata)[];
};
type ModificationTypes =
@@ -18,7 +19,9 @@ type ModificationTypes =
| 'NewProperty'
| 'RemovedProperty'
| 'NewField'
- | 'RemovedField';
+ | 'RemovedField'
+ | 'NewCustomMetadataRecord'
+ | 'RemovedCustomMetadataRecord';
export type MemberModificationType = {
__typename: ModificationTypes;
@@ -105,7 +108,10 @@ function getNewOrModifiedApexMembers(oldVersion: VersionManifest, newVersion: Ve
function getCustomObjectModifications(oldVersion: VersionManifest, newVersion: VersionManifest): NewOrModifiedMember[] {
return pipe(
getCustomObjectsInBothVersions(oldVersion, newVersion),
- (customObjectsInBoth) => getNewOrRemovedCustomFields(customObjectsInBoth),
+ (customObjectsInBoth) => [
+ ...getNewOrRemovedCustomFields(customObjectsInBoth),
+ ...getNewOrRemovedCustomMetadataRecords(customObjectsInBoth),
+ ],
(customObjectModifications) => customObjectModifications.filter((member) => member.modifications.length > 0),
);
}
@@ -179,6 +185,21 @@ function getNewOrRemovedCustomFields(typesInBoth: TypeInBoth[]): NewOrModifiedMember[] {
+ return typesInBoth.map(({ oldType, newType }) => {
+ const oldCustomObject = oldType;
+ const newCustomObject = newType;
+
+ return {
+ typeName: newType.name,
+ modifications: [
+ ...getNewValues(oldCustomObject, newCustomObject, 'metadataRecords', 'NewCustomMetadataRecord'),
+ ...getRemovedValues(oldCustomObject, newCustomObject, 'metadataRecords', 'RemovedCustomMetadataRecord'),
+ ],
+ };
+ });
+}
+
function getNewOrModifiedEnumValues(typesInBoth: TypeInBoth[]): NewOrModifiedMember[] {
return pipe(
typesInBoth.filter((typeInBoth): typeInBoth is TypeInBoth => typeInBoth.oldType.type_name === 'enum'),
diff --git a/src/core/changelog/renderable-changelog.ts b/src/core/changelog/renderable-changelog.ts
index 0b72ed1c..825feccb 100644
--- a/src/core/changelog/renderable-changelog.ts
+++ b/src/core/changelog/renderable-changelog.ts
@@ -4,6 +4,7 @@ import { RenderableContent } from '../renderables/types';
import { adaptDescribable } from '../renderables/documentables';
import { CustomObjectMetadata } from '../reflection/sobject/reflect-custom-object-sources';
import { CustomFieldMetadata } from '../reflection/sobject/reflect-custom-field-source';
+import { CustomMetadataMetadata } from '../reflection/sobject/reflect-custom-metadata-source';
type NewTypeRenderable = {
name: string;
@@ -43,11 +44,12 @@ export type RenderableChangelog = {
newCustomObjects: NewTypeSection<'customobject'> | null;
removedCustomObjects: RemovedTypeSection | null;
newOrRemovedCustomFields: NewOrModifiedMembersSection | null;
+ newOrRemovedCustomMetadataTypeRecords: NewOrModifiedMembersSection | null;
};
export function convertToRenderableChangelog(
changelog: Changelog,
- newManifest: (Type | CustomObjectMetadata | CustomFieldMetadata)[],
+ newManifest: (Type | CustomObjectMetadata | CustomFieldMetadata | CustomMetadataMetadata)[],
): RenderableChangelog {
const allNewTypes = [...changelog.newApexTypes, ...changelog.newCustomObjects].map(
(newType) => newManifest.find((type) => type.name.toLowerCase() === newType.toLowerCase())!,
@@ -59,6 +61,16 @@ export function convertToRenderableChangelog(
const newCustomObjects = allNewTypes.filter(
(type): type is CustomObjectMetadata => type.type_name === 'customobject',
);
+ const newOrModifiedCustomFields = changelog.customObjectModifications.filter(
+ (modification): modification is NewOrModifiedMember =>
+ modification.modifications.some((mod) => mod.__typename === 'NewField' || mod.__typename === 'RemovedField'),
+ );
+ const newOrModifiedCustomMetadataTypeRecords = changelog.customObjectModifications.filter(
+ (modification): modification is NewOrModifiedMember =>
+ modification.modifications.some(
+ (mod) => mod.__typename === 'NewCustomMetadataRecord' || mod.__typename === 'RemovedCustomMetadataRecord',
+ ),
+ );
return {
newClasses:
@@ -121,11 +133,19 @@ export function convertToRenderableChangelog(
}
: null,
newOrRemovedCustomFields:
- changelog.customObjectModifications.length > 0
+ newOrModifiedCustomFields.length > 0
? {
heading: 'New or Removed Fields to Custom Objects or Standard Objects',
description: 'These custom fields have been added or removed.',
- modifications: changelog.customObjectModifications.map(toRenderableModification),
+ modifications: newOrModifiedCustomFields.map(toRenderableModification),
+ }
+ : null,
+ newOrRemovedCustomMetadataTypeRecords:
+ newOrModifiedCustomMetadataTypeRecords.length > 0
+ ? {
+ heading: 'New or Removed Custom Metadata Type Records',
+ description: 'These custom metadata type records have been added or removed.',
+ modifications: newOrModifiedCustomMetadataTypeRecords.map(toRenderableModification),
}
: null,
};
@@ -179,5 +199,9 @@ function toRenderableModificationDescription(memberModificationType: MemberModif
return `New Type: ${withDescription(memberModificationType)}`;
case 'RemovedType':
return `Removed Type: ${memberModificationType.name}`;
+ case 'NewCustomMetadataRecord':
+ return `New Custom Metadata Record: ${withDescription(memberModificationType)}`;
+ case 'RemovedCustomMetadataRecord':
+ return `Removed Custom Metadata Record: ${memberModificationType.name}`;
}
}
diff --git a/src/core/changelog/templates/changelog-template.ts b/src/core/changelog/templates/changelog-template.ts
index eb6f431b..26ec2d79 100644
--- a/src/core/changelog/templates/changelog-template.ts
+++ b/src/core/changelog/templates/changelog-template.ts
@@ -95,6 +95,21 @@ export const changelogTemplate = `
- {{this}}
{{/each}}
+{{/each}}
+{{/if}}
+
+{{#if newOrRemovedCustomMetadataTypeRecords}}
+## {{newOrRemovedCustomMetadataTypeRecords.heading}}
+
+{{newOrRemovedCustomMetadataTypeRecords.description}}
+
+{{#each newOrRemovedCustomMetadataTypeRecords.modifications}}
+### {{this.typeName}}
+
+{{#each this.modifications}}
+- {{this}}
+{{/each}}
+
{{/each}}
{{/if}}
`.trim();
diff --git a/src/core/markdown/__test__/generating-custom-object-docs.spec.ts b/src/core/markdown/__test__/generating-custom-object-docs.spec.ts
index feef912e..572c2901 100644
--- a/src/core/markdown/__test__/generating-custom-object-docs.spec.ts
+++ b/src/core/markdown/__test__/generating-custom-object-docs.spec.ts
@@ -1,7 +1,10 @@
import { extendExpect } from './expect-extensions';
import { customFieldPickListValues, generateDocs, unparsedObjectBundleFromRawString } from './test-helpers';
import { assertEither } from '../../test-helpers/assert-either';
-import { unparsedFieldBundleFromRawString } from '../../test-helpers/test-data-builders';
+import {
+ unparsedCustomMetadataFromRawString,
+ unparsedFieldBundleFromRawString,
+} from '../../test-helpers/test-data-builders';
import { CustomObjectXmlBuilder } from '../../test-helpers/test-data-builders/custom-object-xml-builder';
describe('Generates Custom Object documentation', () => {
@@ -136,5 +139,73 @@ describe('Generates Custom Object documentation', () => {
expect(result).documentationBundleHasLength(1);
assertEither(result, (data) => expect(data).firstDocContains('`TestField__c`'));
});
+
+ describe('when documenting Custom Metadata Types', () => {
+ it('displays the Records heading if fields are present', async () => {
+ const customObjectBundle = unparsedObjectBundleFromRawString({
+ name: 'TestObject__mdt',
+ rawContent: new CustomObjectXmlBuilder().build(),
+ filePath: 'src/object/TestObject__mdt.object-meta.xml',
+ });
+
+ const customMetadataBundle = unparsedCustomMetadataFromRawString({
+ filePath: 'src/customMetadata/TestField__c.field-meta.xml',
+ parentName: 'TestObject',
+ apiName: 'TestObject.TestField__c',
+ });
+
+ const result = await generateDocs([customObjectBundle, customMetadataBundle])();
+ expect(result).documentationBundleHasLength(1);
+ assertEither(result, (data) => expect(data).firstDocContains('## Records'));
+ });
+
+ it('does not display the Records heading if no records are present', async () => {
+ const input = unparsedObjectBundleFromRawString({
+ name: 'TestObject__mdt',
+ rawContent: new CustomObjectXmlBuilder().build(),
+ filePath: 'src/object/TestObject__c.object-meta.xml',
+ });
+
+ const result = await generateDocs([input])();
+ expect(result).documentationBundleHasLength(1);
+ assertEither(result, (data) => expect(data).not.firstDocContains('## Records'));
+ });
+
+ it('displays the record label as a heading', async () => {
+ const customObjectBundle = unparsedObjectBundleFromRawString({
+ name: 'TestObject__mdt',
+ rawContent: new CustomObjectXmlBuilder().build(),
+ filePath: 'src/object/TestObject__mdt.object-meta.xml',
+ });
+
+ const customMetadataBundle = unparsedCustomMetadataFromRawString({
+ filePath: 'src/customMetadata/TestField__c.field-meta.xml',
+ parentName: 'TestObject',
+ apiName: 'TestObject.TestField__c',
+ });
+
+ const result = await generateDocs([customObjectBundle, customMetadataBundle])();
+ expect(result).documentationBundleHasLength(1);
+ assertEither(result, (data) => expect(data).firstDocContains('## Test Metadata'));
+ });
+
+ it('displays the record api name', async () => {
+ const customObjectBundle = unparsedObjectBundleFromRawString({
+ name: 'TestObject__mdt',
+ rawContent: new CustomObjectXmlBuilder().build(),
+ filePath: 'src/object/TestObject__mdt.object-meta.xml',
+ });
+
+ const customMetadataBundle = unparsedCustomMetadataFromRawString({
+ filePath: 'src/customMetadata/TestField__c.field-meta.xml',
+ parentName: 'TestObject',
+ apiName: 'TestObject.TestField__c',
+ });
+
+ const result = await generateDocs([customObjectBundle, customMetadataBundle])();
+ expect(result).documentationBundleHasLength(1);
+ assertEither(result, (data) => expect(data).firstDocContains('TestObject.TestField__c'));
+ });
+ });
});
});
diff --git a/src/core/markdown/__test__/test-helpers.ts b/src/core/markdown/__test__/test-helpers.ts
index 2f126de6..0ed98827 100644
--- a/src/core/markdown/__test__/test-helpers.ts
+++ b/src/core/markdown/__test__/test-helpers.ts
@@ -15,17 +15,18 @@ export function unparsedApexBundleFromRawString(raw: string, rawMetadata?: strin
export function unparsedObjectBundleFromRawString(meta: {
rawContent: string;
filePath: string;
+ name?: string;
}): UnparsedCustomObjectBundle {
return {
type: 'customobject',
- name: 'TestObject__c',
+ name: meta.name ?? 'TestObject__c',
filePath: meta.filePath,
content: meta.rawContent,
};
}
-export function generateDocs(apexBundles: UnparsedSourceBundle[], config?: Partial) {
- return gen(apexBundles, {
+export function generateDocs(bundles: UnparsedSourceBundle[], config?: Partial) {
+ return gen(bundles, {
targetDir: 'target',
scope: ['global', 'public'],
defaultGroupName: 'Miscellaneous',
diff --git a/src/core/markdown/adapters/type-to-renderable.ts b/src/core/markdown/adapters/type-to-renderable.ts
index 46226a0f..39220d51 100644
--- a/src/core/markdown/adapters/type-to-renderable.ts
+++ b/src/core/markdown/adapters/type-to-renderable.ts
@@ -12,6 +12,7 @@ import {
GetRenderableContentByTypeName,
RenderableCustomObject,
RenderableCustomField,
+ RenderableCustomMetadata,
} from '../../renderables/types';
import { adaptDescribable, adaptDocumentable } from '../../renderables/documentables';
import { adaptConstructor, adaptMethod } from './methods-and-constructors';
@@ -21,6 +22,7 @@ import { ExternalMetadata, SourceFileMetadata } from '../../shared/types';
import { CustomObjectMetadata } from '../../reflection/sobject/reflect-custom-object-sources';
import { getTypeGroup, isInSource } from '../../shared/utils';
import { CustomFieldMetadata } from '../../reflection/sobject/reflect-custom-field-source';
+import { CustomMetadataMetadata } from '../../reflection/sobject/reflect-custom-metadata-source';
type GetReturnRenderable = T extends InterfaceMirror
? RenderableInterface
@@ -266,6 +268,12 @@ function objectMetadataToRenderable(
heading: 'Fields',
value: objectMetadata.fields.map((field) => fieldMetadataToRenderable(field, config, 3)),
},
+ hasRecords: objectMetadata.metadataRecords.length > 0,
+ metadataRecords: {
+ headingLevel: 2,
+ heading: 'Records',
+ value: objectMetadata.metadataRecords.map((metadata) => customMetadataToRenderable(metadata, 3)),
+ },
};
}
@@ -292,6 +300,17 @@ function fieldMetadataToRenderable(
};
}
+function customMetadataToRenderable(metadata: CustomMetadataMetadata, headingLevel: number): RenderableCustomMetadata {
+ return {
+ type: 'metadata',
+ headingLevel: headingLevel,
+ heading: metadata.label ?? metadata.name,
+ apiName: metadata.apiName,
+ label: metadata.label ?? metadata.name,
+ protected: metadata.protected,
+ };
+}
+
function getApiName(currentName: string, config: MarkdownGeneratorConfig) {
if (config.namespace) {
// first remove any `__c` suffix
diff --git a/src/core/markdown/generate-docs.ts b/src/core/markdown/generate-docs.ts
index fbffc05a..ef7f6e42 100644
--- a/src/core/markdown/generate-docs.ts
+++ b/src/core/markdown/generate-docs.ts
@@ -32,8 +32,8 @@ import { removeExcludedTags } from '../reflection/apex/remove-excluded-tags';
import { HookError } from '../errors/errors';
import { CustomObjectMetadata } from '../reflection/sobject/reflect-custom-object-sources';
import { Type } from '@cparra/apex-reflection';
-import { reflectCustomFieldsAndObjects } from '../reflection/sobject/reflectCustomFieldsAndObjects';
-import { filterApexSourceFiles, filterCustomObjectsAndFields } from '#utils/source-bundle-utils';
+import { reflectCustomFieldsAndObjectsAndMetadataRecords } from '../reflection/sobject/reflectCustomFieldsAndObjectsAndMetadataRecords';
+import { filterApexSourceFiles, filterCustomObjectsFieldsAndMetadataRecords } from '#utils/source-bundle-utils';
export type MarkdownGeneratorConfig = Omit<
UserDefinedMarkdownConfig,
@@ -52,9 +52,10 @@ export function generateDocs(unparsedBundles: UnparsedSourceBundle[], config: Ma
);
const sort = apply(sortTypesAndMembers, config.sortAlphabetically);
- function filterOutCustomFields(parsedFiles: ParsedFile[]): ParsedFile[] {
+ function filterOutCustomFieldsAndMetadata(parsedFiles: ParsedFile[]): ParsedFile[] {
return parsedFiles.filter(
- (parsedFile): parsedFile is ParsedFile => parsedFile.source.type !== 'customfield',
+ (parsedFile): parsedFile is ParsedFile =>
+ parsedFile.source.type !== 'customfield' && parsedFile.source.type !== 'custommetadata',
);
}
@@ -62,11 +63,11 @@ export function generateDocs(unparsedBundles: UnparsedSourceBundle[], config: Ma
generateForApex(filterApexSourceFiles(unparsedBundles), config),
TE.chain((parsedApexFiles) => {
return pipe(
- reflectCustomFieldsAndObjects(filterCustomObjectsAndFields(unparsedBundles)),
+ reflectCustomFieldsAndObjectsAndMetadataRecords(filterCustomObjectsFieldsAndMetadataRecords(unparsedBundles)),
TE.map((parsedObjectFiles) => [...parsedApexFiles, ...parsedObjectFiles]),
);
}),
- TE.map((parsedFiles) => sort(filterOutCustomFields(parsedFiles))),
+ TE.map((parsedFiles) => sort(filterOutCustomFieldsAndMetadata(parsedFiles))),
TE.bindTo('parsedFiles'),
TE.bind('references', ({ parsedFiles }) =>
TE.right(
@@ -75,7 +76,9 @@ export function generateDocs(unparsedBundles: UnparsedSourceBundle[], config: Ma
),
),
TE.flatMap(({ parsedFiles, references }) => transformReferenceHook(config)({ references, parsedFiles })),
- TE.map(({ parsedFiles, references }) => convertToRenderableBundle(filterOutCustomFields(parsedFiles), references)),
+ TE.map(({ parsedFiles, references }) =>
+ convertToRenderableBundle(filterOutCustomFieldsAndMetadata(parsedFiles), references),
+ ),
TE.map(convertToDocumentationBundleForTemplate),
TE.flatMap(transformDocumentationBundleHook(config)),
TE.map(postHookCompile),
diff --git a/src/core/markdown/templates/custom-object-template.ts b/src/core/markdown/templates/custom-object-template.ts
index 16d2f99c..f33022fd 100644
--- a/src/core/markdown/templates/custom-object-template.ts
+++ b/src/core/markdown/templates/custom-object-template.ts
@@ -39,4 +39,25 @@ export const customObjectTemplate = `
{{/each}}
{{/if}}
+{{#if hasRecords}}
+{{ heading metadataRecords.headingLevel metadataRecords.heading }}
+{{#each metadataRecords.value}}
+{{ heading headingLevel heading }}
+
+{{#if protected}}
+\`Protected\`
+{{/if}}
+
+{{#if description}}
+{{{renderContent description}}}
+{{/if}}
+
+**API Name**
+
+\`{{{apiName}}}\`
+
+{{#unless @last}}---{{/unless}}
+{{/each}}
+{{/if}}
+
`.trim();
diff --git a/src/core/reflection/sobject/reflect-custom-metadata-source.ts b/src/core/reflection/sobject/reflect-custom-metadata-source.ts
new file mode 100644
index 00000000..7ed4f1c9
--- /dev/null
+++ b/src/core/reflection/sobject/reflect-custom-metadata-source.ts
@@ -0,0 +1,86 @@
+import { ParsedFile, UnparsedCustomMetadataBundle } from '../../shared/types';
+import * as TE from 'fp-ts/TaskEither';
+import { ReflectionError, ReflectionErrors } from '../../errors/errors';
+import { pipe } from 'fp-ts/function';
+import * as A from 'fp-ts/Array';
+import * as E from 'fp-ts/Either';
+import { XMLParser } from 'fast-xml-parser';
+
+export type CustomMetadataMetadata = {
+ type_name: 'custommetadata';
+ protected: boolean;
+ apiName: string;
+ name: string;
+ label?: string | null;
+ parentName: string;
+};
+
+export function reflectCustomMetadataSources(
+ customMetadataSources: UnparsedCustomMetadataBundle[],
+): TE.TaskEither[]> {
+ return pipe(customMetadataSources, A.traverse(TE.ApplicativePar)(reflectCustomMetadataSource));
+}
+
+function reflectCustomMetadataSource(
+ customMetadataSource: UnparsedCustomMetadataBundle,
+): TE.TaskEither> {
+ return pipe(
+ E.tryCatch(() => new XMLParser().parse(customMetadataSource.content), E.toError),
+ E.flatMap(validate),
+ E.map(toCustomMetadataMetadata),
+ E.map((metadata) => addNames(metadata, customMetadataSource.name, customMetadataSource.apiName)),
+ E.map((metadata) => addParentName(metadata, customMetadataSource.parentName)),
+ E.map((metadata) => toParsedFile(customMetadataSource.filePath, metadata)),
+ E.mapLeft((error) => new ReflectionErrors([new ReflectionError(customMetadataSource.filePath, error.message)])),
+ TE.fromEither,
+ );
+}
+
+function validate(parsedResult: unknown): E.Either {
+ const err = E.left(new Error('Invalid custom metadata'));
+
+ function isObject(value: unknown) {
+ return typeof value === 'object' && value !== null ? E.right(value) : err;
+ }
+
+ function hasTheCustomMetadataKey(value: object) {
+ return 'CustomMetadata' in value ? E.right(value) : err;
+ }
+
+ return pipe(parsedResult, isObject, E.chain(hasTheCustomMetadataKey));
+}
+
+function toCustomMetadataMetadata(parserResult: { CustomMetadata: unknown }): CustomMetadataMetadata {
+ const customMetadata =
+ parserResult?.CustomMetadata != null && typeof parserResult.CustomMetadata === 'object'
+ ? parserResult.CustomMetadata
+ : {};
+ const defaultValues: Partial = {
+ label: null,
+ };
+
+ return {
+ ...defaultValues,
+ ...customMetadata,
+ type_name: 'custommetadata',
+ } as CustomMetadataMetadata;
+}
+
+function addNames(metadata: CustomMetadataMetadata, name: string, apiName: string): CustomMetadataMetadata {
+ return { ...metadata, name, apiName };
+}
+
+function addParentName(metadata: CustomMetadataMetadata, parentName: string): CustomMetadataMetadata {
+ return { ...metadata, parentName };
+}
+
+function toParsedFile(filePath: string, typeMirror: CustomMetadataMetadata): ParsedFile {
+ return {
+ source: {
+ filePath,
+ name: typeMirror.name,
+ type: typeMirror.type_name,
+ },
+ type: typeMirror,
+ };
+}
diff --git a/src/core/reflection/sobject/reflect-custom-object-sources.ts b/src/core/reflection/sobject/reflect-custom-object-sources.ts
index 6f22780d..5654d6e8 100644
--- a/src/core/reflection/sobject/reflect-custom-object-sources.ts
+++ b/src/core/reflection/sobject/reflect-custom-object-sources.ts
@@ -9,6 +9,7 @@ import * as A from 'fp-ts/Array';
import * as E from 'fp-ts/Either';
import { CustomFieldMetadata } from './reflect-custom-field-source';
import { getPickListValues } from './parse-picklist-values';
+import { CustomMetadataMetadata } from './reflect-custom-metadata-source';
export type CustomObjectMetadata = {
type_name: 'customobject';
@@ -18,6 +19,7 @@ export type CustomObjectMetadata = {
name: string;
description: string | null;
fields: CustomFieldMetadata[];
+ metadataRecords: CustomMetadataMetadata[];
};
export function reflectCustomObjectSources(
@@ -64,11 +66,12 @@ function validate(parseResult: unknown): E.Either = {
deploymentStatus: 'Deployed',
visibility: 'Public',
description: null,
fields: [] as CustomFieldMetadata[],
+ metadataRecords: [] as CustomMetadataMetadata[],
};
return { ...defaultValues, ...customObject } as CustomObjectMetadata;
}
diff --git a/src/core/reflection/sobject/reflectCustomFieldsAndObjects.ts b/src/core/reflection/sobject/reflectCustomFieldsAndObjectsAndMetadataRecords.ts
similarity index 73%
rename from src/core/reflection/sobject/reflectCustomFieldsAndObjects.ts
rename to src/core/reflection/sobject/reflectCustomFieldsAndObjectsAndMetadataRecords.ts
index 083865a4..157f8354 100644
--- a/src/core/reflection/sobject/reflectCustomFieldsAndObjects.ts
+++ b/src/core/reflection/sobject/reflectCustomFieldsAndObjectsAndMetadataRecords.ts
@@ -1,13 +1,19 @@
-import { ParsedFile, UnparsedCustomFieldBundle, UnparsedCustomObjectBundle } from '../../shared/types';
+import {
+ ParsedFile,
+ UnparsedCustomFieldBundle,
+ UnparsedCustomMetadataBundle,
+ UnparsedCustomObjectBundle,
+} from '../../shared/types';
import { CustomObjectMetadata, reflectCustomObjectSources } from './reflect-custom-object-sources';
import * as TE from 'fp-ts/TaskEither';
import { ReflectionErrors } from '../../errors/errors';
import { CustomFieldMetadata, reflectCustomFieldSources } from './reflect-custom-field-source';
import { pipe } from 'fp-ts/function';
import { TaskEither } from 'fp-ts/TaskEither';
+import { CustomMetadataMetadata, reflectCustomMetadataSources } from './reflect-custom-metadata-source';
-export function reflectCustomFieldsAndObjects(
- objectBundles: (UnparsedCustomObjectBundle | UnparsedCustomFieldBundle)[],
+export function reflectCustomFieldsAndObjectsAndMetadataRecords(
+ objectBundles: (UnparsedCustomObjectBundle | UnparsedCustomFieldBundle | UnparsedCustomMetadataBundle)[],
): TaskEither[]> {
function filterNonPublished(parsedFiles: ParsedFile[]): ParsedFile[] {
return parsedFiles.filter((parsedFile) => parsedFile.type.deploymentStatus === 'Deployed');
@@ -25,12 +31,22 @@ export function reflectCustomFieldsAndObjects(
(object): object is UnparsedCustomFieldBundle => object.type === 'customfield',
);
+ const customMetadata = objectBundles.filter(
+ (object): object is UnparsedCustomMetadataBundle => object.type === 'custommetadata',
+ );
+
function generateForFields(
fields: UnparsedCustomFieldBundle[],
): TE.TaskEither[]> {
return pipe(fields, reflectCustomFieldSources);
}
+ function generateForMetadata(
+ metadata: UnparsedCustomMetadataBundle[],
+ ): TE.TaskEither[]> {
+ return pipe(metadata, reflectCustomMetadataSources);
+ }
+
return pipe(
customObjects,
reflectCustomObjectSources,
@@ -38,24 +54,29 @@ export function reflectCustomFieldsAndObjects(
TE.map(filterNonPublic),
TE.bindTo('objects'),
TE.bind('fields', () => generateForFields(customFields)),
- TE.map(({ objects, fields }) => {
- return [...mapFieldsToObjects(objects, fields), ...mapExtensionFields(objects, fields)];
+ TE.bind('metadata', () => generateForMetadata(customMetadata)),
+ TE.map(({ objects, fields, metadata }) => {
+ return [...mapFieldsAndMetadata(objects, fields, metadata), ...mapExtensionFields(objects, fields)];
}),
);
}
-function mapFieldsToObjects(
+function mapFieldsAndMetadata(
objects: ParsedFile[],
fields: ParsedFile[],
+ metadata: ParsedFile[],
): ParsedFile[] {
// Locate the fields for each object by using the parentName property
return objects.map((object) => {
const objectFields = fields.filter((field) => field.type.parentName === object.type.name);
+ const objectMetadata = metadata.filter((meta) => `${meta.type.parentName}__mdt` === object.type.name);
+
return {
...object,
type: {
...object.type,
fields: [...object.type.fields, ...objectFields.map((field) => field.type)],
+ metadataRecords: [...object.type.metadataRecords, ...objectMetadata.map((meta) => meta.type)],
},
};
});
@@ -97,6 +118,7 @@ function mapExtensionFields(
name: key,
description: null,
fields: fields,
+ metadataRecords: [],
},
};
});
diff --git a/src/core/renderables/types.d.ts b/src/core/renderables/types.d.ts
index f54c5fad..5f1bd03a 100644
--- a/src/core/renderables/types.d.ts
+++ b/src/core/renderables/types.d.ts
@@ -181,7 +181,9 @@ export type RenderableCustomObject = Omit & {
apiName: string;
type: 'customobject';
hasFields: boolean;
+ hasRecords: boolean;
fields: RenderableSection;
+ metadataRecords: RenderableSection;
};
export type RenderableCustomField = {
@@ -195,6 +197,15 @@ export type RenderableCustomField = {
required: boolean;
};
+export type RenderableCustomMetadata = {
+ headingLevel: number;
+ heading: string;
+ apiName: string;
+ type: 'metadata';
+ label: string;
+ protected: boolean;
+};
+
export type Renderable = (RenderableClass | RenderableInterface | RenderableEnum | RenderableCustomObject) & {
filePath: string | undefined;
};
diff --git a/src/core/shared/types.d.ts b/src/core/shared/types.d.ts
index 0e3157db..e2fdff5c 100644
--- a/src/core/shared/types.d.ts
+++ b/src/core/shared/types.d.ts
@@ -1,6 +1,7 @@
import { Type } from '@cparra/apex-reflection';
import { CustomObjectMetadata } from '../reflection/sobject/reflect-custom-object-sources';
import { CustomFieldMetadata } from '../reflection/sobject/reflect-custom-field-source';
+import { CustomMetadataMetadata } from '../reflection/sobject/reflect-custom-metadata-source';
export type Generators = 'markdown' | 'openapi' | 'changelog';
@@ -29,7 +30,7 @@ export type CliConfigurableMarkdownConfig = {
};
export type UserDefinedMarkdownConfig = {
- targetGenerator: 'markdown' /** Glob patterns to exclude files from the documentation. */;
+ targetGenerator: 'markdown';
excludeTags: string[];
exclude: string[];
} & CliConfigurableMarkdownConfig &
@@ -59,7 +60,11 @@ export type UserDefinedChangelogConfig = {
export type UserDefinedConfig = UserDefinedMarkdownConfig | UserDefinedOpenApiConfig | UserDefinedChangelogConfig;
-export type UnparsedSourceBundle = UnparsedApexBundle | UnparsedCustomObjectBundle | UnparsedCustomFieldBundle;
+export type UnparsedSourceBundle =
+ | UnparsedApexBundle
+ | UnparsedCustomObjectBundle
+ | UnparsedCustomFieldBundle
+ | UnparsedCustomMetadataBundle;
export type UnparsedCustomObjectBundle = {
type: 'customobject';
@@ -76,6 +81,15 @@ export type UnparsedCustomFieldBundle = {
parentName: string;
};
+export type UnparsedCustomMetadataBundle = {
+ type: 'custommetadata';
+ apiName: string;
+ name: string;
+ filePath: string;
+ content: string;
+ parentName: string;
+};
+
export type UnparsedApexBundle = {
type: 'apex';
name: string;
@@ -84,7 +98,7 @@ export type UnparsedApexBundle = {
metadataContent: string | null;
};
-type MetadataTypes = 'interface' | 'class' | 'enum' | 'customobject' | 'customfield';
+type MetadataTypes = 'interface' | 'class' | 'enum' | 'customobject' | 'customfield' | 'custommetadata';
export type SourceFileMetadata = {
filePath: string;
@@ -104,7 +118,11 @@ export type ExternalMetadata = {
};
export type ParsedFile<
- T extends Type | CustomObjectMetadata | CustomFieldMetadata = Type | CustomObjectMetadata | CustomFieldMetadata,
+ T extends Type | CustomObjectMetadata | CustomFieldMetadata | CustomMetadataMetadata =
+ | Type
+ | CustomObjectMetadata
+ | CustomFieldMetadata
+ | CustomMetadataMetadata,
> = {
source: SourceFileMetadata | ExternalMetadata;
type: T;
diff --git a/src/core/test-helpers/test-data-builders.ts b/src/core/test-helpers/test-data-builders.ts
index 4bd4a20e..9c0bed2e 100644
--- a/src/core/test-helpers/test-data-builders.ts
+++ b/src/core/test-helpers/test-data-builders.ts
@@ -1,4 +1,4 @@
-import { UnparsedCustomFieldBundle } from '../shared/types';
+import { UnparsedCustomFieldBundle, UnparsedCustomMetadataBundle } from '../shared/types';
export const customField = `
@@ -25,3 +25,31 @@ export function unparsedFieldBundleFromRawString(meta: {
parentName: meta.parentName,
};
}
+
+export const customMetadata = `
+
+
+
+ true
+
+ Field1__c
+ Sample Value
+
+
+`;
+
+export function unparsedCustomMetadataFromRawString(meta: {
+ rawContent?: string;
+ filePath: string;
+ apiName: string;
+ parentName: string;
+}): UnparsedCustomMetadataBundle {
+ return {
+ type: 'custommetadata',
+ name: meta.apiName,
+ filePath: meta.filePath,
+ content: meta.rawContent ?? customMetadata,
+ apiName: meta.apiName,
+ parentName: meta.parentName,
+ };
+}
diff --git a/src/util/source-bundle-utils.ts b/src/util/source-bundle-utils.ts
index 39fcb650..dc63676c 100644
--- a/src/util/source-bundle-utils.ts
+++ b/src/util/source-bundle-utils.ts
@@ -1,6 +1,7 @@
import {
UnparsedApexBundle,
UnparsedCustomFieldBundle,
+ UnparsedCustomMetadataBundle,
UnparsedCustomObjectBundle,
UnparsedSourceBundle,
} from '../core/shared/types';
@@ -9,11 +10,11 @@ export function filterApexSourceFiles(sourceFiles: UnparsedSourceBundle[]): Unpa
return sourceFiles.filter((sourceFile): sourceFile is UnparsedApexBundle => sourceFile.type === 'apex');
}
-export function filterCustomObjectsAndFields(
+export function filterCustomObjectsFieldsAndMetadataRecords(
sourceFiles: UnparsedSourceBundle[],
-): (UnparsedCustomObjectBundle | UnparsedCustomFieldBundle)[] {
+): (UnparsedCustomObjectBundle | UnparsedCustomFieldBundle | UnparsedCustomMetadataBundle)[] {
return sourceFiles.filter(
(sourceFile): sourceFile is UnparsedCustomObjectBundle =>
- sourceFile.type === 'customobject' || sourceFile.type === 'customfield',
+ sourceFile.type === 'customobject' || sourceFile.type === 'customfield' || sourceFile.type === 'custommetadata',
);
}