From a65618e5ae9f0ae753a3672c09d0de94a069a494 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Tue, 4 Feb 2025 08:24:59 -0400 Subject: [PATCH 1/9] Custom metadata records are picked up for documentation --- .../docs/.vitepress/cache/deps/_metadata.json | 14 +-- .../vitepress/docs/.vitepress/sidebar.json | 40 +++++++++ examples/vitepress/docs/changelog.md | 25 ++++++ .../docs/custom-objects/VisibleCMT__mdt.md | 20 +++++ examples/vitepress/docs/index.md | 32 +++++++ .../VisibleCMT.Some_Record_1.md-meta.xml | 9 ++ .../SameNamespaceMDT__mdt.object-meta.xml | 6 ++ .../VisibleCMT__mdt.object-meta.xml | 6 ++ .../fields/Field1__c.field-meta.xml | 11 +++ src/application/Apexdocs.ts | 7 +- src/application/source-code-file-reader.ts | 59 ++++++++++++- src/core/changelog/generate-change-log.ts | 4 +- .../markdown/adapters/type-to-renderable.ts | 2 + src/core/markdown/generate-docs.ts | 11 ++- .../sobject/reflect-custom-metadata-source.ts | 88 +++++++++++++++++++ .../sobject/reflect-custom-object-sources.ts | 5 +- ...stomFieldsAndObjectsAndMetadataRecords.ts} | 49 +++++++++-- src/core/shared/types.d.ts | 26 ++++-- src/util/source-bundle-utils.ts | 12 ++- 19 files changed, 395 insertions(+), 31 deletions(-) create mode 100644 examples/vitepress/docs/custom-objects/VisibleCMT__mdt.md create mode 100644 examples/vitepress/force-app/main/default/customMetadata/VisibleCMT.Some_Record_1.md-meta.xml create mode 100644 examples/vitepress/force-app/main/default/objects/SameNamespaceMDT__mdt/SameNamespaceMDT__mdt.object-meta.xml create mode 100644 examples/vitepress/force-app/main/default/objects/VisibleCMT__mdt/VisibleCMT__mdt.object-meta.xml create mode 100644 examples/vitepress/force-app/main/default/objects/VisibleCMT__mdt/fields/Field1__c.field-meta.xml create mode 100644 src/core/reflection/sobject/reflect-custom-metadata-source.ts rename src/core/reflection/sobject/{reflectCustomFieldsAndObjects.ts => reflectCustomFieldsAndObjectsAndMetadataRecords.ts} (68%) 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..521440e0 100644 --- a/examples/vitepress/docs/.vitepress/sidebar.json +++ b/examples/vitepress/docs/.vitepress/sidebar.json @@ -66,10 +66,22 @@ "text": "Event__c", "link": "custom-objects/Event__c.md" }, + { + "text": "Event__c", + "link": "custom-objects/Event__c.md" + }, { "text": "Price_Component__c", "link": "custom-objects/Price_Component__c.md" }, + { + "text": "Price_Component__c", + "link": "custom-objects/Price_Component__c.md" + }, + { + "text": "Product__c", + "link": "custom-objects/Product__c.md" + }, { "text": "Product__c", "link": "custom-objects/Product__c.md" @@ -78,6 +90,14 @@ "text": "Product_Inline_Fields__c", "link": "custom-objects/Product_Inline_Fields__c.md" }, + { + "text": "Product_Inline_Fields__c", + "link": "custom-objects/Product_Inline_Fields__c.md" + }, + { + "text": "Product_Price_Component__c", + "link": "custom-objects/Product_Price_Component__c.md" + }, { "text": "Product_Price_Component__c", "link": "custom-objects/Product_Price_Component__c.md" @@ -86,6 +106,14 @@ "text": "Sales_Order__c", "link": "custom-objects/Sales_Order__c.md" }, + { + "text": "Sales_Order__c", + "link": "custom-objects/Sales_Order__c.md" + }, + { + "text": "Sales_Order_Line__c", + "link": "custom-objects/Sales_Order_Line__c.md" + }, { "text": "Sales_Order_Line__c", "link": "custom-objects/Sales_Order_Line__c.md" @@ -93,6 +121,18 @@ { "text": "Speaker__c", "link": "custom-objects/Speaker__c.md" + }, + { + "text": "Speaker__c", + "link": "custom-objects/Speaker__c.md" + }, + { + "text": "VisibleCMT__mdt", + "link": "custom-objects/VisibleCMT__mdt.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..f9a2c330 100644 --- a/examples/vitepress/docs/changelog.md +++ b/examples/vitepress/docs/changelog.md @@ -72,6 +72,31 @@ Custom object for tracking sales orders. ### Speaker__c Represents a speaker at an event. +### VisibleCMT__mdt + +### Event__c + +Represents an event that people can register for. +### Price_Component__c + +### Product_Inline_Fields__c + +Products +### Product_Price_Component__c + +### Product__c + +Product that is sold or available for sale. +### Sales_Order_Line__c + +Represents a line item on a sales order. +### Sales_Order__c + +Custom object for tracking sales orders. +### Speaker__c + +Represents a speaker at an event. +### VisibleCMT__mdt ## New or Removed Fields to Custom Objects or Standard Objects 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..ae1a3ebb --- /dev/null +++ b/examples/vitepress/docs/custom-objects/VisibleCMT__mdt.md @@ -0,0 +1,20 @@ +--- +title: VisibleCMT__mdt +--- + +# VisibleCMT + +## API Name +`VisibleCMT__mdt` + +## Fields +### Field1 +**Required** + +**API Name** + +`apexdocs__Field1__c` + +**Type** + +*Text* \ No newline at end of file diff --git a/examples/vitepress/docs/index.md b/examples/vitepress/docs/index.md index 6d8ce3bb..626090a2 100644 --- a/examples/vitepress/docs/index.md +++ b/examples/vitepress/docs/index.md @@ -25,22 +25,46 @@ hero: Represents an event that people can register for. +### [Event__c](custom-objects/Event__c) + +Represents an event that people can register for. + +### [Price_Component__c](custom-objects/Price_Component__c) + ### [Price_Component__c](custom-objects/Price_Component__c) ### [Product__c](custom-objects/Product__c) Product that is sold or available for sale. +### [Product__c](custom-objects/Product__c) + +Product that is sold or available for sale. + ### [Product_Inline_Fields__c](custom-objects/Product_Inline_Fields__c) Products +### [Product_Inline_Fields__c](custom-objects/Product_Inline_Fields__c) + +Products + +### [Product_Price_Component__c](custom-objects/Product_Price_Component__c) + ### [Product_Price_Component__c](custom-objects/Product_Price_Component__c) ### [Sales_Order__c](custom-objects/Sales_Order__c) Custom object for tracking sales orders. +### [Sales_Order__c](custom-objects/Sales_Order__c) + +Custom object for tracking sales orders. + +### [Sales_Order_Line__c](custom-objects/Sales_Order_Line__c) + +Represents a line item on a sales order. + ### [Sales_Order_Line__c](custom-objects/Sales_Order_Line__c) Represents a line item on a sales order. @@ -49,6 +73,14 @@ Represents a line item on a sales order. Represents a speaker at an event. +### [Speaker__c](custom-objects/Speaker__c) + +Represents a speaker at an event. + +### [VisibleCMT__mdt](custom-objects/VisibleCMT__mdt) + +### [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/src/application/Apexdocs.ts b/src/application/Apexdocs.ts index e317b345..56550feb 100644 --- a/src/application/Apexdocs.ts +++ b/src/application/Apexdocs.ts @@ -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(['ApexClass', 'CustomObject', 'CustomField', 'CustomMetadata'], { + includeMetadata: config.includeMetadata, + })(config.sourceDir, config.exclude), (e) => new FileReadingError('An error occurred while reading files.', e), ), TE.fromEither, diff --git a/src/application/source-code-file-reader.ts b/src/application/source-code-file-reader.ts index a2a81ae6..a16d2429 100644 --- a/src/application/source-code-file-reader.ts +++ b/src/application/source-code-file-reader.ts @@ -1,10 +1,15 @@ 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'; +type ComponentTypes = 'ApexClass' | 'CustomObject' | 'CustomField' | 'CustomMetadata'; /** * Simplified representation of a source component, with only @@ -43,6 +48,13 @@ type CustomFieldSourceComponent = { parentName: string; }; +type CustomMetadataSourceComponent = { + type: 'CustomMetadata'; + name: string; + contentPath: string; + parentName: string; +}; + function getApexSourceComponents( includeMetadata: boolean, sourceComponents: SourceComponentAdapter[], @@ -125,6 +137,39 @@ 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) => ({ + 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) => ({ + 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 +182,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 +198,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/generate-change-log.ts b/src/core/changelog/generate-change-log.ts index a11d7daa..0a79b6e4 100644 --- a/src/core/changelog/generate-change-log.ts +++ b/src/core/changelog/generate-change-log.ts @@ -18,7 +18,7 @@ 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'; @@ -70,7 +70,7 @@ function reflect(bundles: UnparsedSourceBundle[], config: Omit { return pipe( - reflectCustomFieldsAndObjects(filterCustomObjectsAndFields(bundles)), + reflectCustomFieldsAndObjectsAndMetadataRecords(filterCustomObjectsAndFields(bundles)), TE.map((parsedObjectFiles) => [...parsedApexFiles, ...parsedObjectFiles]), ); }), diff --git a/src/core/markdown/adapters/type-to-renderable.ts b/src/core/markdown/adapters/type-to-renderable.ts index 46226a0f..0eb0bad8 100644 --- a/src/core/markdown/adapters/type-to-renderable.ts +++ b/src/core/markdown/adapters/type-to-renderable.ts @@ -250,6 +250,8 @@ function objectMetadataToRenderable( objectMetadata: CustomObjectMetadata, config: MarkdownGeneratorConfig, ): RenderableCustomObject { + console.log(JSON.stringify(objectMetadata, null, 2)); + return { type: 'customobject', headingLevel: 1, diff --git a/src/core/markdown/generate-docs.ts b/src/core/markdown/generate-docs.ts index fbffc05a..b99878d1 100644 --- a/src/core/markdown/generate-docs.ts +++ b/src/core/markdown/generate-docs.ts @@ -32,8 +32,12 @@ 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, + filterCustomObjectsAndFields, + filterCustomObjectsFieldsAndMetadataRecords, +} from '#utils/source-bundle-utils'; export type MarkdownGeneratorConfig = Omit< UserDefinedMarkdownConfig, @@ -62,10 +66,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]), ); }), + // TODO: Sort out custom metadata whenever is necessary TE.map((parsedFiles) => sort(filterOutCustomFields(parsedFiles))), TE.bindTo('parsedFiles'), TE.bind('references', ({ parsedFiles }) => 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..4b8e179c --- /dev/null +++ b/src/core/reflection/sobject/reflect-custom-metadata-source.ts @@ -0,0 +1,88 @@ +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; + name: string; + label?: string | null; + description: string | null; + parentName: string; + // TODO: Reflect values +}; + +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) => addName(metadata, customMetadataSource.name)), + 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 = { + label: null, + description: null, + }; + + return { + ...defaultValues, + ...customMetadata, + type_name: 'custommetadata', + } as CustomMetadataMetadata; +} + +function addName(metadata: CustomMetadataMetadata, name: string): CustomMetadataMetadata { + return { ...metadata, name }; +} + +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 68% rename from src/core/reflection/sobject/reflectCustomFieldsAndObjects.ts rename to src/core/reflection/sobject/reflectCustomFieldsAndObjectsAndMetadataRecords.ts index 083865a4..3121c94c 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,8 +54,13 @@ 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 [ + ...mapFieldsToObjects(objects, fields), + ...mapCustomMetadataToObjects(objects, metadata), + ...mapExtensionFields(objects, fields), + ]; }), ); } @@ -61,6 +82,23 @@ function mapFieldsToObjects( }); } +function mapCustomMetadataToObjects( + objects: ParsedFile[], + metadata: ParsedFile[], +): ParsedFile[] { + // Locate the metadata for each object by using the parentName property + return objects.map((object) => { + const objectMetadata = metadata.filter((meta) => `${meta.type.parentName}__mdt` === object.type.name); + return { + ...object, + type: { + ...object.type, + metadataRecords: [...object.type.metadataRecords, ...objectMetadata.map((meta) => meta.type)], + }, + }; + }); +} + // "Extension" fields are fields that are in the source code without the corresponding object-meta.xml file. // These are fields that either extend a standard Salesforce object, or an object in a different package. function mapExtensionFields( @@ -97,6 +135,7 @@ function mapExtensionFields( name: key, description: null, fields: fields, + metadataRecords: [], }, }; }); diff --git a/src/core/shared/types.d.ts b/src/core/shared/types.d.ts index 0e3157db..2dd5ccf4 100644 --- a/src/core/shared/types.d.ts +++ b/src/core/shared/types.d.ts @@ -1,6 +1,6 @@ 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 { CustomFieldMetadata, CustomMetadataMetadata } from '../reflection/sobject/reflect-custom-field-source'; export type Generators = 'markdown' | 'openapi' | 'changelog'; @@ -29,7 +29,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 +59,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 +80,14 @@ export type UnparsedCustomFieldBundle = { parentName: string; }; +export type UnparsedCustomMetadataBundle = { + type: 'custommetadata'; + name: string; + filePath: string; + content: string; + parentName: string; +}; + export type UnparsedApexBundle = { type: 'apex'; name: string; @@ -84,7 +96,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 +116,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/util/source-bundle-utils.ts b/src/util/source-bundle-utils.ts index 39fcb650..9eae0ede 100644 --- a/src/util/source-bundle-utils.ts +++ b/src/util/source-bundle-utils.ts @@ -1,6 +1,6 @@ import { UnparsedApexBundle, - UnparsedCustomFieldBundle, + UnparsedCustomFieldBundle, UnparsedCustomMetadataBundle, UnparsedCustomObjectBundle, UnparsedSourceBundle, } from '../core/shared/types'; @@ -9,6 +9,7 @@ export function filterApexSourceFiles(sourceFiles: UnparsedSourceBundle[]): Unpa return sourceFiles.filter((sourceFile): sourceFile is UnparsedApexBundle => sourceFile.type === 'apex'); } +// TODO: The changelog still uses this export function filterCustomObjectsAndFields( sourceFiles: UnparsedSourceBundle[], ): (UnparsedCustomObjectBundle | UnparsedCustomFieldBundle)[] { @@ -17,3 +18,12 @@ export function filterCustomObjectsAndFields( sourceFile.type === 'customobject' || sourceFile.type === 'customfield', ); } + +export function filterCustomObjectsFieldsAndMetadataRecords( + sourceFiles: UnparsedSourceBundle[], +): (UnparsedCustomObjectBundle | UnparsedCustomFieldBundle | UnparsedCustomMetadataBundle)[] { + return sourceFiles.filter( + (sourceFile): sourceFile is UnparsedCustomObjectBundle => + sourceFile.type === 'customobject' || sourceFile.type === 'customfield' || sourceFile.type === 'custommetadata', + ); +} From 03875434ca7eb940128643071c0bb89452b4fe9e Mon Sep 17 00:00:00 2001 From: cesarParra Date: Tue, 4 Feb 2025 10:41:17 -0400 Subject: [PATCH 2/9] Preventing objects to appear twice in the reference guide --- .../vitepress/docs/.vitepress/sidebar.json | 36 ------------------- examples/vitepress/docs/changelog.md | 24 ------------- .../docs/custom-objects/VisibleCMT__mdt.md | 9 ++++- examples/vitepress/docs/index.md | 30 ---------------- src/application/source-code-file-reader.ts | 3 ++ src/core/changelog/generate-change-log.ts | 8 +++-- src/core/changelog/process-changelog.ts | 3 +- src/core/changelog/renderable-changelog.ts | 3 +- .../markdown/adapters/type-to-renderable.ts | 22 ++++++++++-- src/core/markdown/generate-docs.ts | 18 +++++----- .../templates/custom-object-template.ts | 17 +++++++++ .../sobject/reflect-custom-metadata-source.ts | 7 ++-- ...ustomFieldsAndObjectsAndMetadataRecords.ts | 27 +++----------- src/core/renderables/types.d.ts | 13 +++++++ src/core/shared/types.d.ts | 4 ++- 15 files changed, 91 insertions(+), 133 deletions(-) diff --git a/examples/vitepress/docs/.vitepress/sidebar.json b/examples/vitepress/docs/.vitepress/sidebar.json index 521440e0..0fc24fcc 100644 --- a/examples/vitepress/docs/.vitepress/sidebar.json +++ b/examples/vitepress/docs/.vitepress/sidebar.json @@ -66,22 +66,10 @@ "text": "Event__c", "link": "custom-objects/Event__c.md" }, - { - "text": "Event__c", - "link": "custom-objects/Event__c.md" - }, { "text": "Price_Component__c", "link": "custom-objects/Price_Component__c.md" }, - { - "text": "Price_Component__c", - "link": "custom-objects/Price_Component__c.md" - }, - { - "text": "Product__c", - "link": "custom-objects/Product__c.md" - }, { "text": "Product__c", "link": "custom-objects/Product__c.md" @@ -90,14 +78,6 @@ "text": "Product_Inline_Fields__c", "link": "custom-objects/Product_Inline_Fields__c.md" }, - { - "text": "Product_Inline_Fields__c", - "link": "custom-objects/Product_Inline_Fields__c.md" - }, - { - "text": "Product_Price_Component__c", - "link": "custom-objects/Product_Price_Component__c.md" - }, { "text": "Product_Price_Component__c", "link": "custom-objects/Product_Price_Component__c.md" @@ -106,14 +86,6 @@ "text": "Sales_Order__c", "link": "custom-objects/Sales_Order__c.md" }, - { - "text": "Sales_Order__c", - "link": "custom-objects/Sales_Order__c.md" - }, - { - "text": "Sales_Order_Line__c", - "link": "custom-objects/Sales_Order_Line__c.md" - }, { "text": "Sales_Order_Line__c", "link": "custom-objects/Sales_Order_Line__c.md" @@ -122,14 +94,6 @@ "text": "Speaker__c", "link": "custom-objects/Speaker__c.md" }, - { - "text": "Speaker__c", - "link": "custom-objects/Speaker__c.md" - }, - { - "text": "VisibleCMT__mdt", - "link": "custom-objects/VisibleCMT__mdt.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 f9a2c330..e20ac3a4 100644 --- a/examples/vitepress/docs/changelog.md +++ b/examples/vitepress/docs/changelog.md @@ -74,30 +74,6 @@ Custom object for tracking sales orders. Represents a speaker at an event. ### VisibleCMT__mdt -### Event__c - -Represents an event that people can register for. -### Price_Component__c - -### Product_Inline_Fields__c - -Products -### Product_Price_Component__c - -### Product__c - -Product that is sold or available for sale. -### Sales_Order_Line__c - -Represents a line item on a sales order. -### Sales_Order__c - -Custom object for tracking sales orders. -### Speaker__c - -Represents a speaker at an event. -### VisibleCMT__mdt - ## New or Removed Fields to Custom Objects or Standard Objects These custom fields have been added or removed. diff --git a/examples/vitepress/docs/custom-objects/VisibleCMT__mdt.md b/examples/vitepress/docs/custom-objects/VisibleCMT__mdt.md index ae1a3ebb..ca747a07 100644 --- a/examples/vitepress/docs/custom-objects/VisibleCMT__mdt.md +++ b/examples/vitepress/docs/custom-objects/VisibleCMT__mdt.md @@ -17,4 +17,11 @@ title: VisibleCMT__mdt **Type** -*Text* \ No newline at end of file +*Text* + +## Records +### Some Record 1 + +**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 626090a2..ca295921 100644 --- a/examples/vitepress/docs/index.md +++ b/examples/vitepress/docs/index.md @@ -25,46 +25,22 @@ hero: Represents an event that people can register for. -### [Event__c](custom-objects/Event__c) - -Represents an event that people can register for. - -### [Price_Component__c](custom-objects/Price_Component__c) - ### [Price_Component__c](custom-objects/Price_Component__c) ### [Product__c](custom-objects/Product__c) Product that is sold or available for sale. -### [Product__c](custom-objects/Product__c) - -Product that is sold or available for sale. - ### [Product_Inline_Fields__c](custom-objects/Product_Inline_Fields__c) Products -### [Product_Inline_Fields__c](custom-objects/Product_Inline_Fields__c) - -Products - -### [Product_Price_Component__c](custom-objects/Product_Price_Component__c) - ### [Product_Price_Component__c](custom-objects/Product_Price_Component__c) ### [Sales_Order__c](custom-objects/Sales_Order__c) Custom object for tracking sales orders. -### [Sales_Order__c](custom-objects/Sales_Order__c) - -Custom object for tracking sales orders. - -### [Sales_Order_Line__c](custom-objects/Sales_Order_Line__c) - -Represents a line item on a sales order. - ### [Sales_Order_Line__c](custom-objects/Sales_Order_Line__c) Represents a line item on a sales order. @@ -73,12 +49,6 @@ Represents a line item on a sales order. Represents a speaker at an event. -### [Speaker__c](custom-objects/Speaker__c) - -Represents a speaker at an event. - -### [VisibleCMT__mdt](custom-objects/VisibleCMT__mdt) - ### [VisibleCMT__mdt](custom-objects/VisibleCMT__mdt) ## Miscellaneous diff --git a/src/application/source-code-file-reader.ts b/src/application/source-code-file-reader.ts index a16d2429..b01f796f 100644 --- a/src/application/source-code-file-reader.ts +++ b/src/application/source-code-file-reader.ts @@ -50,6 +50,7 @@ type CustomFieldSourceComponent = { type CustomMetadataSourceComponent = { type: 'CustomMetadata'; + apiName: string; name: string; contentPath: string; parentName: string; @@ -150,6 +151,7 @@ function getCustomMetadataSourceComponents( return sourceComponents .filter((component) => component.type.name === 'CustomMetadata') .map((component) => ({ + apiName: component.name, name: getParentAndNamePair(component)[1], type: 'CustomMetadata' as const, contentPath: component.xml!, @@ -162,6 +164,7 @@ function toUnparsedCustomMetadataBundle( customMetadataSourceComponents: CustomMetadataSourceComponent[], ): UnparsedCustomMetadataBundle[] { return customMetadataSourceComponents.map((component) => ({ + apiName: component.apiName, type: 'custommetadata', name: component.name, filePath: component.contentPath, diff --git a/src/core/changelog/generate-change-log.ts b/src/core/changelog/generate-change-log.ts index 0a79b6e4..d48c9c0a 100644 --- a/src/core/changelog/generate-change-log.ts +++ b/src/core/changelog/generate-change-log.ts @@ -25,6 +25,7 @@ import { filterApexSourceFiles, filterCustomObjectsAndFields } from '#utils/sour 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; @@ -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..086584b7 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 = diff --git a/src/core/changelog/renderable-changelog.ts b/src/core/changelog/renderable-changelog.ts index 0b72ed1c..2ec86c49 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; @@ -47,7 +48,7 @@ export type RenderableChangelog = { 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())!, diff --git a/src/core/markdown/adapters/type-to-renderable.ts b/src/core/markdown/adapters/type-to-renderable.ts index 0eb0bad8..647c6037 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 @@ -250,8 +252,6 @@ function objectMetadataToRenderable( objectMetadata: CustomObjectMetadata, config: MarkdownGeneratorConfig, ): RenderableCustomObject { - console.log(JSON.stringify(objectMetadata, null, 2)); - return { type: 'customobject', headingLevel: 1, @@ -268,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)), + }, }; } @@ -294,6 +300,18 @@ function fieldMetadataToRenderable( }; } +function customMetadataToRenderable(metadata: CustomMetadataMetadata, headingLevel: number): RenderableCustomMetadata { + return { + type: 'metadata', + headingLevel: headingLevel, + heading: metadata.label ?? metadata.name, + description: metadata.description ? [metadata.description] : [], + 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 b99878d1..ef7f6e42 100644 --- a/src/core/markdown/generate-docs.ts +++ b/src/core/markdown/generate-docs.ts @@ -33,11 +33,7 @@ import { HookError } from '../errors/errors'; import { CustomObjectMetadata } from '../reflection/sobject/reflect-custom-object-sources'; import { Type } from '@cparra/apex-reflection'; import { reflectCustomFieldsAndObjectsAndMetadataRecords } from '../reflection/sobject/reflectCustomFieldsAndObjectsAndMetadataRecords'; -import { - filterApexSourceFiles, - filterCustomObjectsAndFields, - filterCustomObjectsFieldsAndMetadataRecords, -} from '#utils/source-bundle-utils'; +import { filterApexSourceFiles, filterCustomObjectsFieldsAndMetadataRecords } from '#utils/source-bundle-utils'; export type MarkdownGeneratorConfig = Omit< UserDefinedMarkdownConfig, @@ -56,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', ); } @@ -70,8 +67,7 @@ export function generateDocs(unparsedBundles: UnparsedSourceBundle[], config: Ma TE.map((parsedObjectFiles) => [...parsedApexFiles, ...parsedObjectFiles]), ); }), - // TODO: Sort out custom metadata whenever is necessary - TE.map((parsedFiles) => sort(filterOutCustomFields(parsedFiles))), + TE.map((parsedFiles) => sort(filterOutCustomFieldsAndMetadata(parsedFiles))), TE.bindTo('parsedFiles'), TE.bind('references', ({ parsedFiles }) => TE.right( @@ -80,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..261a159f 100644 --- a/src/core/markdown/templates/custom-object-template.ts +++ b/src/core/markdown/templates/custom-object-template.ts @@ -39,4 +39,21 @@ export const customObjectTemplate = ` {{/each}} {{/if}} +{{#if hasRecords}} +{{ heading metadataRecords.headingLevel metadataRecords.heading }} +{{#each metadataRecords.value}} +{{ heading headingLevel heading }} + +{{#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 index 4b8e179c..e6d72e6b 100644 --- a/src/core/reflection/sobject/reflect-custom-metadata-source.ts +++ b/src/core/reflection/sobject/reflect-custom-metadata-source.ts @@ -9,6 +9,7 @@ import { XMLParser } from 'fast-xml-parser'; export type CustomMetadataMetadata = { type_name: 'custommetadata'; protected: boolean; + apiName: string; name: string; label?: string | null; description: string | null; @@ -29,7 +30,7 @@ function reflectCustomMetadataSource( E.tryCatch(() => new XMLParser().parse(customMetadataSource.content), E.toError), E.flatMap(validate), E.map(toCustomMetadataMetadata), - E.map((metadata) => addName(metadata, customMetadataSource.name)), + 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)])), @@ -68,8 +69,8 @@ function toCustomMetadataMetadata(parserResult: { CustomMetadata: unknown }): Cu } as CustomMetadataMetadata; } -function addName(metadata: CustomMetadataMetadata, name: string): CustomMetadataMetadata { - return { ...metadata, name }; +function addNames(metadata: CustomMetadataMetadata, name: string, apiName: string): CustomMetadataMetadata { + return { ...metadata, name, apiName }; } function addParentName(metadata: CustomMetadataMetadata, parentName: string): CustomMetadataMetadata { diff --git a/src/core/reflection/sobject/reflectCustomFieldsAndObjectsAndMetadataRecords.ts b/src/core/reflection/sobject/reflectCustomFieldsAndObjectsAndMetadataRecords.ts index 3121c94c..157f8354 100644 --- a/src/core/reflection/sobject/reflectCustomFieldsAndObjectsAndMetadataRecords.ts +++ b/src/core/reflection/sobject/reflectCustomFieldsAndObjectsAndMetadataRecords.ts @@ -56,43 +56,26 @@ export function reflectCustomFieldsAndObjectsAndMetadataRecords( TE.bind('fields', () => generateForFields(customFields)), TE.bind('metadata', () => generateForMetadata(customMetadata)), TE.map(({ objects, fields, metadata }) => { - return [ - ...mapFieldsToObjects(objects, fields), - ...mapCustomMetadataToObjects(objects, metadata), - ...mapExtensionFields(objects, fields), - ]; + 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); - return { - ...object, - type: { - ...object.type, - fields: [...object.type.fields, ...objectFields.map((field) => field.type)], - }, - }; - }); -} - -function mapCustomMetadataToObjects( - objects: ParsedFile[], - metadata: ParsedFile[], -): ParsedFile[] { - // Locate the metadata for each object by using the parentName property - return objects.map((object) => { 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)], }, }; diff --git a/src/core/renderables/types.d.ts b/src/core/renderables/types.d.ts index f54c5fad..cface860 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,17 @@ export type RenderableCustomField = { required: boolean; }; +export type RenderableCustomMetadata = { + headingLevel: number; + heading: string; + apiName: string; + description: RenderableContent[]; + type: 'metadata'; + label: string; + protected: boolean; + // TODO: Add values? +}; + 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 2dd5ccf4..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, CustomMetadataMetadata } from '../reflection/sobject/reflect-custom-field-source'; +import { CustomFieldMetadata } from '../reflection/sobject/reflect-custom-field-source'; +import { CustomMetadataMetadata } from '../reflection/sobject/reflect-custom-metadata-source'; export type Generators = 'markdown' | 'openapi' | 'changelog'; @@ -82,6 +83,7 @@ export type UnparsedCustomFieldBundle = { export type UnparsedCustomMetadataBundle = { type: 'custommetadata'; + apiName: string; name: string; filePath: string; content: string; From b760ebdbceb54807f2a3fbc5e0afae625b3f2d2a Mon Sep 17 00:00:00 2001 From: cesarParra Date: Tue, 4 Feb 2025 10:44:30 -0400 Subject: [PATCH 3/9] Records describe if they are protected or not --- examples/vitepress/docs/custom-objects/VisibleCMT__mdt.md | 2 ++ src/core/markdown/templates/custom-object-template.ts | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/examples/vitepress/docs/custom-objects/VisibleCMT__mdt.md b/examples/vitepress/docs/custom-objects/VisibleCMT__mdt.md index ca747a07..edc0ffa8 100644 --- a/examples/vitepress/docs/custom-objects/VisibleCMT__mdt.md +++ b/examples/vitepress/docs/custom-objects/VisibleCMT__mdt.md @@ -22,6 +22,8 @@ title: VisibleCMT__mdt ## Records ### Some Record 1 +`Protected` + **API Name** `VisibleCMT.Some_Record_1` \ No newline at end of file diff --git a/src/core/markdown/templates/custom-object-template.ts b/src/core/markdown/templates/custom-object-template.ts index 261a159f..f33022fd 100644 --- a/src/core/markdown/templates/custom-object-template.ts +++ b/src/core/markdown/templates/custom-object-template.ts @@ -44,6 +44,10 @@ export const customObjectTemplate = ` {{#each metadataRecords.value}} {{ heading headingLevel heading }} +{{#if protected}} +\`Protected\` +{{/if}} + {{#if description}} {{{renderContent description}}} {{/if}} From b162278f8017039fce411dba8b6cedd06ecc5d85 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Tue, 4 Feb 2025 11:09:02 -0400 Subject: [PATCH 4/9] Custom Metadata rendering tests --- .../__test__/processing-changelog.spec.ts | 1 + .../generating-custom-object-docs.spec.ts | 73 ++++++++++++++++++- src/core/markdown/__test__/test-helpers.ts | 7 +- .../markdown/adapters/type-to-renderable.ts | 1 - .../sobject/reflect-custom-metadata-source.ts | 5 +- src/core/renderables/types.d.ts | 2 - src/core/test-helpers/test-data-builders.ts | 30 +++++++- 7 files changed, 107 insertions(+), 12 deletions(-) diff --git a/src/core/changelog/__test__/processing-changelog.spec.ts b/src/core/changelog/__test__/processing-changelog.spec.ts index ad8ea5a1..04c00d47 100644 --- a/src/core/changelog/__test__/processing-changelog.spec.ts +++ b/src/core/changelog/__test__/processing-changelog.spec.ts @@ -57,6 +57,7 @@ class CustomObjectMetadataBuilder { name: 'MyObject', description: null, fields: this.fields, + metadataRecords: [], }; } } 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 647c6037..39220d51 100644 --- a/src/core/markdown/adapters/type-to-renderable.ts +++ b/src/core/markdown/adapters/type-to-renderable.ts @@ -305,7 +305,6 @@ function customMetadataToRenderable(metadata: CustomMetadataMetadata, headingLev type: 'metadata', headingLevel: headingLevel, heading: metadata.label ?? metadata.name, - description: metadata.description ? [metadata.description] : [], apiName: metadata.apiName, label: metadata.label ?? metadata.name, protected: metadata.protected, diff --git a/src/core/reflection/sobject/reflect-custom-metadata-source.ts b/src/core/reflection/sobject/reflect-custom-metadata-source.ts index e6d72e6b..7ed4f1c9 100644 --- a/src/core/reflection/sobject/reflect-custom-metadata-source.ts +++ b/src/core/reflection/sobject/reflect-custom-metadata-source.ts @@ -12,9 +12,7 @@ export type CustomMetadataMetadata = { apiName: string; name: string; label?: string | null; - description: string | null; parentName: string; - // TODO: Reflect values }; export function reflectCustomMetadataSources( @@ -57,9 +55,8 @@ function toCustomMetadataMetadata(parserResult: { CustomMetadata: unknown }): Cu parserResult?.CustomMetadata != null && typeof parserResult.CustomMetadata === 'object' ? parserResult.CustomMetadata : {}; - const defaultValues = { + const defaultValues: Partial = { label: null, - description: null, }; return { diff --git a/src/core/renderables/types.d.ts b/src/core/renderables/types.d.ts index cface860..5f1bd03a 100644 --- a/src/core/renderables/types.d.ts +++ b/src/core/renderables/types.d.ts @@ -201,11 +201,9 @@ export type RenderableCustomMetadata = { headingLevel: number; heading: string; apiName: string; - description: RenderableContent[]; type: 'metadata'; label: string; protected: boolean; - // TODO: Add values? }; export type Renderable = (RenderableClass | RenderableInterface | RenderableEnum | RenderableCustomObject) & { 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, + }; +} From 411b134e30444f8b734fd30459c5d5fa538522d6 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Tue, 4 Feb 2025 14:34:50 -0400 Subject: [PATCH 5/9] Custom Metadata Type records appear in the changelog --- examples/vitepress/docs/changelog.md | 11 ++++++-- .../VisibleCMT__mdt.object-meta.xml | 6 +++++ .../fields/Field1__c.field-meta.xml | 11 ++++++++ src/application/Apexdocs.ts | 10 +++++-- src/core/changelog/generate-change-log.ts | 4 +-- src/core/changelog/process-changelog.ts | 24 +++++++++++++++-- src/core/changelog/renderable-changelog.ts | 27 +++++++++++++++++-- .../changelog/templates/changelog-template.ts | 15 +++++++++++ src/util/source-bundle-utils.ts | 13 ++------- 9 files changed, 100 insertions(+), 21 deletions(-) create mode 100644 examples/vitepress/previous/force-app/main/default/objects/VisibleCMT__mdt/VisibleCMT__mdt.object-meta.xml create mode 100644 examples/vitepress/previous/force-app/main/default/objects/VisibleCMT__mdt/fields/Field1__c.field-meta.xml diff --git a/examples/vitepress/docs/changelog.md b/examples/vitepress/docs/changelog.md index e20ac3a4..915a7aae 100644 --- a/examples/vitepress/docs/changelog.md +++ b/examples/vitepress/docs/changelog.md @@ -72,7 +72,6 @@ Custom object for tracking sales orders. ### Speaker__c Represents a speaker at an event. -### VisibleCMT__mdt ## New or Removed Fields to Custom Objects or Standard Objects @@ -80,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/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/src/application/Apexdocs.ts b/src/application/Apexdocs.ts index 56550feb..a397c309 100644 --- a/src/application/Apexdocs.ts +++ b/src/application/Apexdocs.ts @@ -72,8 +72,14 @@ 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(['ApexClass', 'CustomObject', 'CustomField', 'CustomMetadata'])( + config.previousVersionDir, + config.exclude, + ), + readFiles(['ApexClass', 'CustomObject', 'CustomField', 'CustomMetadata'])( + config.currentVersionDir, + config.exclude, + ), ]; } diff --git a/src/core/changelog/generate-change-log.ts b/src/core/changelog/generate-change-log.ts index d48c9c0a..380f443b 100644 --- a/src/core/changelog/generate-change-log.ts +++ b/src/core/changelog/generate-change-log.ts @@ -21,7 +21,7 @@ import { isInSource, isSkip, passThroughHook, skip, toFrontmatterString } from ' 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'; @@ -71,7 +71,7 @@ function reflect(bundles: UnparsedSourceBundle[], config: Omit { return pipe( - reflectCustomFieldsAndObjectsAndMetadataRecords(filterCustomObjectsAndFields(bundles)), + reflectCustomFieldsAndObjectsAndMetadataRecords(filterCustomObjectsFieldsAndMetadataRecords(bundles)), TE.map((parsedObjectFiles) => [...parsedApexFiles, ...parsedObjectFiles]), ); }), diff --git a/src/core/changelog/process-changelog.ts b/src/core/changelog/process-changelog.ts index 086584b7..64aeab26 100644 --- a/src/core/changelog/process-changelog.ts +++ b/src/core/changelog/process-changelog.ts @@ -19,7 +19,9 @@ type ModificationTypes = | 'NewProperty' | 'RemovedProperty' | 'NewField' - | 'RemovedField'; + | 'RemovedField' + | 'NewCustomMetadataRecord' + | 'RemovedCustomMetadataRecord'; export type MemberModificationType = { __typename: ModificationTypes; @@ -106,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), ); } @@ -180,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 2ec86c49..825feccb 100644 --- a/src/core/changelog/renderable-changelog.ts +++ b/src/core/changelog/renderable-changelog.ts @@ -44,6 +44,7 @@ export type RenderableChangelog = { newCustomObjects: NewTypeSection<'customobject'> | null; removedCustomObjects: RemovedTypeSection | null; newOrRemovedCustomFields: NewOrModifiedMembersSection | null; + newOrRemovedCustomMetadataTypeRecords: NewOrModifiedMembersSection | null; }; export function convertToRenderableChangelog( @@ -60,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: @@ -122,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, }; @@ -180,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/util/source-bundle-utils.ts b/src/util/source-bundle-utils.ts index 9eae0ede..dc63676c 100644 --- a/src/util/source-bundle-utils.ts +++ b/src/util/source-bundle-utils.ts @@ -1,6 +1,7 @@ import { UnparsedApexBundle, - UnparsedCustomFieldBundle, UnparsedCustomMetadataBundle, + UnparsedCustomFieldBundle, + UnparsedCustomMetadataBundle, UnparsedCustomObjectBundle, UnparsedSourceBundle, } from '../core/shared/types'; @@ -9,16 +10,6 @@ export function filterApexSourceFiles(sourceFiles: UnparsedSourceBundle[]): Unpa return sourceFiles.filter((sourceFile): sourceFile is UnparsedApexBundle => sourceFile.type === 'apex'); } -// TODO: The changelog still uses this -export function filterCustomObjectsAndFields( - sourceFiles: UnparsedSourceBundle[], -): (UnparsedCustomObjectBundle | UnparsedCustomFieldBundle)[] { - return sourceFiles.filter( - (sourceFile): sourceFile is UnparsedCustomObjectBundle => - sourceFile.type === 'customobject' || sourceFile.type === 'customfield', - ); -} - export function filterCustomObjectsFieldsAndMetadataRecords( sourceFiles: UnparsedSourceBundle[], ): (UnparsedCustomObjectBundle | UnparsedCustomFieldBundle | UnparsedCustomMetadataBundle)[] { From 6515faa2aa884d1de98eef975b9c356f3338dd17 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Tue, 4 Feb 2025 16:44:12 -0400 Subject: [PATCH 6/9] Changelog unit tests --- .../helpers/custom-field-metadata-builder.ts | 28 ++++ .../custom-metadata-metadata-builder.ts | 22 ++++ .../helpers/custom-object-metadata-builder.ts | 32 +++++ .../__test__/processing-changelog.spec.ts | 122 ++++++++++-------- 4 files changed, 152 insertions(+), 52 deletions(-) create mode 100644 src/core/changelog/__test__/helpers/custom-field-metadata-builder.ts create mode 100644 src/core/changelog/__test__/helpers/custom-metadata-metadata-builder.ts create mode 100644 src/core/changelog/__test__/helpers/custom-object-metadata-builder.ts 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 04c00d47..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,56 +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, - metadataRecords: [], - }; - } -} - describe('when generating a changelog', () => { it('has no new types when both the old and new versions are empty', () => { const oldVersion = { types: [] }; @@ -669,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', + }, + ], + }, + ]); + }); + }); }); From 3fe9d70ad70c0d50d366626be88e6718422607f9 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Tue, 4 Feb 2025 16:51:24 -0400 Subject: [PATCH 7/9] Doc updates --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) 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. From 5f9f0fd2846ac8f8e35d851e18cc44bd5c8bbca3 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Tue, 4 Feb 2025 16:57:41 -0400 Subject: [PATCH 8/9] Refactorings --- src/application/Apexdocs.ts | 14 ++++---------- src/application/source-code-file-reader.ts | 3 ++- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/application/Apexdocs.ts b/src/application/Apexdocs.ts index a397c309..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,7 +52,7 @@ async function processMarkdown(config: UserDefinedMarkdownConfig) { return pipe( E.tryCatch( () => - readFiles(['ApexClass', 'CustomObject', 'CustomField', 'CustomMetadata'], { + readFiles(allComponentTypes, { includeMetadata: config.includeMetadata, })(config.sourceDir, config.exclude), (e) => new FileReadingError('An error occurred while reading files.', e), @@ -72,14 +72,8 @@ async function processOpenApi(config: UserDefinedOpenApiConfig, logger: Logger) async function processChangeLog(config: UserDefinedChangelogConfig) { function loadFiles(): [UnparsedSourceBundle[], UnparsedSourceBundle[]] { return [ - readFiles(['ApexClass', 'CustomObject', 'CustomField', 'CustomMetadata'])( - config.previousVersionDir, - config.exclude, - ), - readFiles(['ApexClass', 'CustomObject', 'CustomField', 'CustomMetadata'])( - 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 b01f796f..96bcaaf2 100644 --- a/src/application/source-code-file-reader.ts +++ b/src/application/source-code-file-reader.ts @@ -9,7 +9,8 @@ import { minimatch } from 'minimatch'; import { flow, pipe } from 'fp-ts/function'; import { apply } from '#utils/fp'; -type ComponentTypes = 'ApexClass' | 'CustomObject' | 'CustomField' | 'CustomMetadata'; +export type ComponentTypes = 'ApexClass' | 'CustomObject' | 'CustomField' | 'CustomMetadata'; +export const allComponentTypes: ComponentTypes[] = ['ApexClass', 'CustomObject', 'CustomField', 'CustomMetadata']; /** * Simplified representation of a source component, with only From 98521ab42b80985c1c52bf4a3719f396300ac807 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Tue, 4 Feb 2025 17:00:48 -0400 Subject: [PATCH 9/9] 3.8 Release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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",