diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9683d0f5..0a86bf5d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -2,9 +2,9 @@ name: CI on: push: - branches: ["master", "develop"] + branches: [ "master", "develop" ] pull_request: - branches: ["main", "master", "develop"] + branches: [ "main", "master", "develop" ] workflow_dispatch: jobs: @@ -17,6 +17,9 @@ jobs: with: node-version: "20" cache: "npm" + + # Set up GitHub Actions caching for Wireit. + - uses: google/wireit@setup-github-actions-caching/v2 + - run: npm ci - - run: npm run build --if-present - run: npm test diff --git a/.gitignore b/.gitignore index 18a056fc..3ef6256b 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ npm-debug.log /dist/ /lib/ .sfdx/ +.wireit/ # Added by Illuminated Cloud .localdev/ @@ -16,4 +17,4 @@ npm-debug.log target/ /.illuminatedCloud/ **/tsconfig*.json -**/*.tsbuildinfo \ No newline at end of file +**/*.tsbuildinfo diff --git a/README.md b/README.md index 7f0ed99e..24c27518 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,8 @@ apexdocs changelog --previousVersionDir force-app-previous --currentVersionDir f |----------------------------|-------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------|----------| | `--sourceDir` | `-s` | The directory where the source files are located. | N/A | Yes | | `--targetDir` | `-t` | The directory where the generated files will be placed. | `docs` | No | -| `--scope` | `-p` | A list of scopes to document. Values should be separated by a space, e.g --scope global public namespaceaccessible. | `global` | No | +| `--scope` | `-p` | A list of scopes to document. Values should be separated by a space, e.g --scope global public namespaceaccessible. | `[global]` | No | +| `--customObjectVisibility` | `-v` | Controls which custom objects are documented. Values should be separated by a space. | `[public]` | No | | `--defaultGroupName` | N/A | The default group name to use when a group is not specified. | `Miscellaneous` | No | | `--namespace` | N/A | The package namespace, if any. If provided, it will be added to the generated files. | N/A | No | | `--sortAlphabetically` | N/A | Sorts files appearing in the Reference Guide alphabetically, as well as the members of a class, interface or enum alphabetically. If false, the members will be displayed in the same order as the code. | `false` | No | @@ -184,14 +185,15 @@ apexdocs openapi -s force-app -t docs -n MyNamespace --title "My Custom OpenApi #### Flags -| Flag | Alias | Description | Default | Required | -|------------------------|-------|--------------------------------------------------------------------|-------------|----------| -| `--previousVersionDir` | `-p` | The directory location of the previous version of the source code. | N/A | Yes | -| `--currentVersionDir` | `-t` | The directory location of the current version of the source code. | N/A | Yes | -| `--targetDir` | `-t` | The directory location where the changelog file will be generated. | `./docs/` | No | -| `--fileName` | N/A | The name of the changelog file to be generated. | `changelog` | No | -| `--scope` | N/A | The list of scope to respect when generating the changelog. | ['global'] | No | -| `--skipIfNoChanges` | N/A | Whether to skip generating the changelog if there are no changes. | `true` | No | +| Flag | Alias | Description | Default | Required | +|----------------------------|-------|--------------------------------------------------------------------------------------|-------------|----------| +| `--previousVersionDir` | `-p` | The directory location of the previous version of the source code. | N/A | Yes | +| `--currentVersionDir` | `-t` | The directory location of the current version of the source code. | N/A | Yes | +| `--targetDir` | `-t` | The directory location where the changelog file will be generated. | `./docs/` | No | +| `--fileName` | N/A | The name of the changelog file to be generated. | `changelog` | No | +| `--scope` | N/A | The list of scope to respect when generating the changelog. | ['global'] | No | +| `--customObjectVisibility` | `-v` | Controls which custom objects are documented. Values should be separated by a space. | ['public'] | No | +| `--skipIfNoChanges` | N/A | Whether to skip generating the changelog if there are no changes. | `true` | No | #### Sample Usage @@ -309,6 +311,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/changelog/docs/changelog.md b/examples/changelog/docs/changelog.md index b567be19..362fafba 100644 --- a/examples/changelog/docs/changelog.md +++ b/examples/changelog/docs/changelog.md @@ -51,7 +51,7 @@ These members have been added or modified. - New Method: newMethod - Removed Method: deprecatedMethod -## New or Removed Fields in Existing Objects +## New or Removed Fields to Custom Objects or Standard Objects These custom fields have been added or removed. @@ -66,4 +66,8 @@ These custom fields have been added or removed. ### Product__c -- New Field: Description__c \ No newline at end of file +- New Field: Description__c + +### Contact + +- New Field: PhotoUrl__c \ No newline at end of file diff --git a/examples/changelog/package-lock.json b/examples/changelog/package-lock.json index 9b36fb9c..4ada25e7 100644 --- a/examples/changelog/package-lock.json +++ b/examples/changelog/package-lock.json @@ -217,9 +217,9 @@ "license": "MIT" }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "license": "MIT", "dependencies": { "path-key": "^3.1.0", diff --git a/examples/markdown/docs/custom-objects/Event__c.md b/examples/markdown/docs/custom-objects/Event__c.md index 111bcb28..023f2a04 100644 --- a/examples/markdown/docs/custom-objects/Event__c.md +++ b/examples/markdown/docs/custom-objects/Event__c.md @@ -18,6 +18,7 @@ Represents an event that people can register for. --- ### End Date +**Required** **API Name** @@ -29,6 +30,7 @@ Represents an event that people can register for. --- ### Location +**Required** **API Name** @@ -40,6 +42,7 @@ Represents an event that people can register for. --- ### Start Date +**Required** **API Name** diff --git a/examples/markdown/docs/custom-objects/Price_Component__c.md b/examples/markdown/docs/custom-objects/Price_Component__c.md index e2305371..e86e2002 100644 --- a/examples/markdown/docs/custom-objects/Price_Component__c.md +++ b/examples/markdown/docs/custom-objects/Price_Component__c.md @@ -55,6 +55,7 @@ Use this when the Price Component represents a Flat Price. To represent a Percen --- ### Type +**Required** **API Name** @@ -62,4 +63,9 @@ Use this when the Price Component represents a Flat Price. To represent a Percen **Type** -*Picklist* \ No newline at end of file +*Picklist* + +#### Possible values are +* List Price +* Surcharge +* Discount \ No newline at end of file diff --git a/examples/markdown/docs/custom-objects/Product__c.md b/examples/markdown/docs/custom-objects/Product__c.md index 795d2ee1..95bfa4c6 100644 --- a/examples/markdown/docs/custom-objects/Product__c.md +++ b/examples/markdown/docs/custom-objects/Product__c.md @@ -18,6 +18,7 @@ Product that is sold or available for sale. --- ### Event +**Required** **API Name** diff --git a/examples/markdown/docs/custom-objects/Sales_Order_Line__c.md b/examples/markdown/docs/custom-objects/Sales_Order_Line__c.md index da0fb5bf..d99afa40 100644 --- a/examples/markdown/docs/custom-objects/Sales_Order_Line__c.md +++ b/examples/markdown/docs/custom-objects/Sales_Order_Line__c.md @@ -7,6 +7,7 @@ Represents a line item on a sales order. ## Fields ### Amount +**Required** **API Name** @@ -18,6 +19,7 @@ Represents a line item on a sales order. --- ### Product +**Required** **API Name** @@ -51,6 +53,7 @@ Represents a line item on a sales order. --- ### Type +**Required** **API Name** @@ -58,4 +61,8 @@ Represents a line item on a sales order. **Type** -*Picklist* \ No newline at end of file +*Picklist* + +#### Possible values are +* Charge +* Discount \ No newline at end of file diff --git a/examples/open-api/docs/openapi.json b/examples/open-api/docs/openapi.json index 2bec9b4e..f2e60a56 100644 --- a/examples/open-api/docs/openapi.json +++ b/examples/open-api/docs/openapi.json @@ -9,6 +9,574 @@ "url": "/services/apexrest/openapi/" } ], - "paths": {}, - "tags": [] + "paths": { + "/AccountService/": { + "description": "Account related operations", + "get": { + "tags": [ + "Account Service" + ], + "description": "This is a sample HTTP Get method", + "parameters": [ + { + "name": "limit", + "in": "query", + "required": true, + "description": "Limits the number of items on a page", + "schema": { + "type": "integer" + } + }, + { + "name": "complex", + "in": "cookie", + "description": "A more complex schema", + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + } + } + } + ], + "responses": { + "100": { + "description": "Status code 100", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "anotherObject": { + "description": "An object inside of an object", + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "somethingElse": { + "type": "number" + } + } + } + } + } + } + } + }, + "200": { + "description": "Status code 200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The super Id." + }, + "name": { + "type": "string" + }, + "phone": { + "type": "string", + "format": "byte" + } + } + } + } + } + }, + "304": { + "description": "Status code 304", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + } + } + } + }, + "400": { + "description": "Status code 400", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + } + } + } + } + }, + "500": { + "description": "Status code 500", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + } + }, + "post": { + "tags": [ + "Account Service" + ], + "description": "This is a sample HTTP Post method", + "summary": "Posts an Account 2", + "requestBody": { + "description": "This is an example of a request body", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + } + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "limit", + "in": "query", + "required": true, + "description": "Limits the number of items on a page", + "schema": { + "type": "integer" + } + }, + { + "name": "complex", + "in": "cookie", + "description": "A more complex schema", + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + } + } + } + ], + "responses": { + "200": { + "description": "Status code 200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The super Id." + }, + "name": { + "type": "string" + }, + "phone": { + "type": "string", + "format": "byte" + } + } + } + } + } + }, + "304": { + "description": "Status code 304", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + } + } + } + }, + "400": { + "description": "Status code 400", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + } + } + } + } + }, + "500": { + "description": "Status code 500", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Account Service" + ], + "description": "Sample HTTP Delete method with references to other types.", + "parameters": [ + { + "name": "limit", + "in": "header", + "required": true, + "description": "My sample description.", + "schema": { + "$ref": "#/components/schemas/SampleClass" + } + } + ], + "responses": { + "200": { + "description": "Status code 200", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SampleClass" + } + } + } + }, + "304": { + "description": "Status code 304", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChildClass" + } + } + } + }, + "305": { + "description": "Status code 305", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Reference1" + } + } + } + }, + "306": { + "description": "Status code 306", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Reference1_array" + } + } + } + }, + "307": { + "description": "Status code 307", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Reference7_Reference7[untypedObject:Reference2]" + } + } + } + }, + "500": { + "description": "Status code 500", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SampleClass" + } + } + } + } + } + } + }, + "/Contact/": { + "description": "Contact related operations", + "get": { + "tags": [ + "Contact" + ], + "description": "This is a sample HTTP Get method", + "responses": { + "200": { + "description": "Status code 200", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SampleRestResourceWithInnerClass.InnerClass" + } + } + } + } + } + } + }, + "/Order/": { + "description": "Order related operations", + "get": { + "tags": [ + "Order" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "param1": { + "type": "string" + }, + "param2": { + "$ref": "#/components/schemas/Reference1" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Status code 200", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + } + }, + "patch": { + "tags": [ + "Order" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "param1": { + "type": "string" + }, + "param2": { + "$ref": "#/components/schemas/Reference1" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Status code 200", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + } + } + } + }, + "tags": [ + { + "name": "Account Service", + "description": "Account related operations" + }, + { + "name": "Contact", + "description": "Contact related operations" + }, + { + "name": "Order", + "description": "Order related operations" + } + ], + "components": { + "schemas": { + "SampleClass": { + "type": "object", + "properties": { + "MyProp": { + "type": "string", + "description": "This is a String property." + }, + "AnotherProp": { + "type": "number", + "description": "This is a Decimal property." + }, + "listOfStrings": { + "type": "array", + "items": { + "type": "string" + } + }, + "someVariable": { + "type": "string" + }, + "somePrivateStuff": { + "type": "string" + } + } + }, + "ChildClass": { + "type": "object", + "properties": { + "privateStringFromChild": { + "type": "string" + }, + "aPrivateString": { + "type": "string" + } + } + }, + "Reference1": { + "type": "object", + "properties": { + "reference2Member": { + "$ref": "#/components/schemas/Reference2", + "description": "This is a reference 2 member. Lorem." + }, + "reference3Member": { + "$ref": "#/components/schemas/Reference3" + }, + "reference4Collection": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Reference4" + } + }, + "reference5Member": { + "$ref": "#/components/schemas/Reference5" + } + } + }, + "Reference2": { + "type": "object", + "properties": { + "stringMember": { + "type": "string" + }, + "objectReference": { + "$ref": "#/components/schemas/Reference3_array", + "description": "This is an object reference." + } + } + }, + "Reference3_array": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Reference3" + } + }, + "Reference3": { + "type": "object", + "properties": { + "someBoolean": { + "type": "boolean" + } + } + }, + "Reference4": { + "type": "object", + "properties": { + "someString": { + "type": "string" + } + } + }, + "Reference5": { + "type": "object", + "properties": { + "reference6Member": { + "$ref": "#/components/schemas/Reference6" + } + } + }, + "Reference6": { + "type": "object", + "properties": { + "grandChildString": { + "type": "string", + "description": "This is the grandchild description." + } + } + }, + "Reference1_array": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Reference1" + } + }, + "Reference7_Reference7[untypedObject:Reference2]": { + "type": "object", + "properties": { + "untypedObject": { + "$ref": "#/components/schemas/Reference2" + } + } + }, + "SampleRestResourceWithInnerClass.InnerClass": { + "type": "object", + "properties": { + "stringMember": { + "type": "string" + } + } + } + } + } } \ No newline at end of file diff --git a/examples/vitepress/docs/.vitepress/cache/deps/_metadata.json b/examples/vitepress/docs/.vitepress/cache/deps/_metadata.json index 3c6ae1c5..3a5d46ac 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": "38118198", "configHash": "7f7b0dad", - "lockfileHash": "76121266", - "browserHash": "441a8d6a", + "lockfileHash": "09651dfc", + "browserHash": "1bd3b4f7", "optimized": { "vue": { "src": "../../../../node_modules/vue/dist/vue.runtime.esm-bundler.js", "file": "vue.js", - "fileHash": "885cbaa9", + "fileHash": "12c2c40e", "needsInterop": false }, "vitepress > @vue/devtools-api": { "src": "../../../../node_modules/@vue/devtools-api/dist/index.js", "file": "vitepress___@vue_devtools-api.js", - "fileHash": "ff3ba36c", + "fileHash": "7b21a64c", "needsInterop": false }, "vitepress > @vueuse/core": { "src": "../../../../node_modules/@vueuse/core/index.mjs", "file": "vitepress___@vueuse_core.js", - "fileHash": "b6cc6d79", + "fileHash": "5d565a7a", "needsInterop": false }, "@theme/index": { "src": "../../../../node_modules/vitepress/dist/client/theme-default/index.js", "file": "@theme_index.js", - "fileHash": "6b17bcd7", + "fileHash": "e4373905", "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/package-lock.json b/examples/vitepress/package-lock.json index 75eb8588..6908b653 100644 --- a/examples/vitepress/package-lock.json +++ b/examples/vitepress/package-lock.json @@ -2314,10 +2314,11 @@ "license": "MIT" }, "node_modules/vite": { - "version": "5.4.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz", - "integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==", + "version": "5.4.14", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz", + "integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==", "dev": true, + "license": "MIT", "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", 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-lock.json b/package-lock.json index 8ac4c68c..fb3c014a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@cparra/apexdocs", - "version": "3.7.2", + "version": "3.8.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@cparra/apexdocs", - "version": "3.7.2", + "version": "3.8.0", "license": "MIT", "dependencies": { "@cparra/apex-reflection": "2.16.1", @@ -38,10 +38,10 @@ "lint-staged": "^15.2.7", "pkgroll": "^2.4.2", "prettier": "^3.3.2", - "rimraf": "^6.0.1", "ts-jest": "^29.2.0", - "typescript": "^5.5.3", - "typescript-eslint": "^7.16.0" + "typescript": "^5.7.3", + "typescript-eslint": "^7.16.0", + "wireit": "^0.14.10" } }, "node_modules/@ampproject/remapping": { @@ -3347,6 +3347,18 @@ "node": ">=10.0.0" } }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -3667,6 +3679,42 @@ "node": ">=10" } }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -3986,9 +4034,9 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "dependencies": { "path-key": "^3.1.0", @@ -5500,6 +5548,18 @@ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-builtin-module": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", @@ -5722,25 +5782,6 @@ "node": ">=8" } }, - "node_modules/jackspeak": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.1.tgz", - "integrity": "sha512-cub8rahkh0Q/bw1+GxP7aeSe29hHHn2V4m29nnDlvCdlgU+3UGxkZp7Z53jLUdpX3jdTO0nJZUDl3xvbWc2Xog==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", @@ -6992,6 +7033,12 @@ "node": ">=6" } }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true + }, "node_modules/jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -7566,16 +7613,6 @@ "node": ">=8" } }, - "node_modules/lru-cache": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.0.tgz", - "integrity": "sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==", - "dev": true, - "license": "ISC", - "engines": { - "node": "20 || >=22" - } - }, "node_modules/magic-string": { "version": "0.30.10", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz", @@ -7644,11 +7681,10 @@ } }, "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, - "license": "MIT", "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -8119,23 +8155,6 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, - "node_modules/path-scurry": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", - "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -8619,6 +8638,18 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/real-require": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", @@ -8744,50 +8775,6 @@ "dev": true, "license": "MIT" }, - "node_modules/rimraf": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", - "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^11.0.0", - "package-json-from-dist": "^1.0.0" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/glob": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", - "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^4.0.1", - "minimatch": "^10.0.0", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^2.0.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/rollup": { "version": "4.22.4", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz", @@ -9513,10 +9500,9 @@ } }, "node_modules/typescript": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", - "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", - "license": "Apache-2.0", + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -9725,6 +9711,50 @@ "node": ">= 8" } }, + "node_modules/wireit": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/wireit/-/wireit-0.14.10.tgz", + "integrity": "sha512-Y9wiNU92PcyfTcXRYzqjmilKl4Yfg30Jk/dwTN0e64JCkzoIP2QVo6gc8fjYK0gpL0/pq2IW+iMlknHLmLV+MQ==", + "dev": true, + "workspaces": [ + "vscode-extension", + "website" + ], + "dependencies": { + "brace-expansion": "^4.0.0", + "chokidar": "^3.5.3", + "fast-glob": "^3.2.11", + "jsonc-parser": "^3.0.0", + "proper-lockfile": "^4.1.2" + }, + "bin": { + "wireit": "bin/wireit.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/wireit/node_modules/balanced-match": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-3.0.1.tgz", + "integrity": "sha512-vjtV3hiLqYDNRoiAv0zC4QaGAMPomEoq83PRmYIofPswwZurCeWR5LByXm7SyoL0Zh5+2z0+HC7jG8gSZJUh0w==", + "dev": true, + "engines": { + "node": ">= 16" + } + }, + "node_modules/wireit/node_modules/brace-expansion": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-4.0.0.tgz", + "integrity": "sha512-l/mOwLWs7BQIgOKrL46dIAbyCKvPV7YJPDspkuc88rHsZRlg3hptUGdU7Trv0VFP4d3xnSGBQrKu5ZvGB7UeIw==", + "dev": true, + "dependencies": { + "balanced-match": "^3.0.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/package.json b/package.json index d4c505b3..4acf912b 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", @@ -19,14 +19,46 @@ "apexdocs": "./dist/cli/generate.js" }, "scripts": { - "test": "npm run build && jest", + "test": "wireit", "test:cov": "npm run build && jest --coverage", - "build": "rimraf ./dist && npm run lint && tsc --noEmit && pkgroll", - "lint": "eslint \"./src/**/*.{js,ts}\" --quiet --fix", + "build": "wireit", + "lint": "wireit", "prepare": "npm run build", "version": "npm run format && git add -A src", "postversion": "git push && git push --tags" }, + "wireit": { + "lint": { + "command": "eslint \"./src/**/*.{js,ts}\" --quiet --fix", + "files": [ + "src/**/*.ts" + ], + "output": [] + }, + "build": { + "command": "tsc --noEmit --pretty && pkgroll", + "dependencies": [ + "lint" + ], + "files": [ + "src/**/*.ts", + "tsconfig.json" + ], + "output": [ + "dist" + ] + }, + "test": { + "command": "jest", + "dependencies": [ + "build" + ], + "files": [ + "src/**/*.ts" + ], + "output": [] + } + }, "author": "Cesar Parra", "license": "MIT", "repository": { @@ -45,10 +77,10 @@ "lint-staged": "^15.2.7", "pkgroll": "^2.4.2", "prettier": "^3.3.2", - "rimraf": "^6.0.1", "ts-jest": "^29.2.0", - "typescript": "^5.5.3", - "typescript-eslint": "^7.16.0" + "typescript": "^5.7.3", + "typescript-eslint": "^7.16.0", + "wireit": "^0.14.10" }, "husky": { "hooks": { 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/cli/commands/changelog.ts b/src/cli/commands/changelog.ts index 58dcd507..b3ecaafb 100644 --- a/src/cli/commands/changelog.ts +++ b/src/cli/commands/changelog.ts @@ -35,6 +35,14 @@ export const changeLogOptions: { [key: string]: Options } = { 'Values should be separated by a space, e.g --scope global public namespaceaccessible. ' + 'Annotations are supported and should be passed lowercased and without the @ symbol, e.g. namespaceaccessible auraenabled.', }, + customObjectVisibility: { + type: 'string', + array: true, + alias: 'v', + default: changeLogDefaults.customObjectVisibility, + choices: ['public', 'protected', 'packageprotected'], + describe: 'Controls which custom objects are documented. Values should be separated by a space.', + }, skipIfNoChanges: { type: 'boolean', default: changeLogDefaults.skipIfNoChanges, diff --git a/src/cli/commands/markdown.ts b/src/cli/commands/markdown.ts index e029d3b8..34c50637 100644 --- a/src/cli/commands/markdown.ts +++ b/src/cli/commands/markdown.ts @@ -24,6 +24,14 @@ export const markdownOptions: Record { 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..7cd8487b 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,10 @@ function reflect(bundles: UnparsedSourceBundle[], config: Omit { return pipe( - reflectCustomFieldsAndObjects(filterCustomObjectsAndFields(bundles)), + reflectCustomFieldsAndObjectsAndMetadataRecords( + filterCustomObjectsFieldsAndMetadataRecords(bundles), + config.customObjectVisibility, + ), TE.map((parsedObjectFiles) => [...parsedApexFiles, ...parsedObjectFiles]), ); }), @@ -81,7 +85,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 +96,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__/generating-docs.spec.ts b/src/core/markdown/__test__/generating-docs.spec.ts index bce18c9e..2ed1df82 100644 --- a/src/core/markdown/__test__/generating-docs.spec.ts +++ b/src/core/markdown/__test__/generating-docs.spec.ts @@ -160,11 +160,25 @@ describe('When generating documentation', () => { expect(result).documentationBundleHasLength(0); }); - it('does not return non-public custom objects', async () => { - const input = new CustomObjectXmlBuilder().withVisibility('Protected').build(); - - const result = await generateDocs([unparsedObjectBundleFromRawString({ rawContent: input, filePath: 'test' })])(); - expect(result).documentationBundleHasLength(0); + describe('and the custom object visibility', () => { + it('is not set, it does not return non-public custom objects', async () => { + const input = new CustomObjectXmlBuilder().withVisibility('Protected').build(); + + const result = await generateDocs([ + unparsedObjectBundleFromRawString({ rawContent: input, filePath: 'test' }), + ])(); + expect(result).documentationBundleHasLength(0); + }); + + it('is configured, it respects the configured visibility', async () => { + const input = new CustomObjectXmlBuilder().withVisibility('Protected').build(); + + const result = await generateDocs( + [unparsedObjectBundleFromRawString({ rawContent: input, filePath: 'test' })], + { customObjectVisibility: ['protected'] }, + )(); + expect(result).documentationBundleHasLength(1); + }); }); it('do not return files that have an @ignore in the docs', async () => { diff --git a/src/core/markdown/__test__/test-helpers.ts b/src/core/markdown/__test__/test-helpers.ts index 2f126de6..e71b998c 100644 --- a/src/core/markdown/__test__/test-helpers.ts +++ b/src/core/markdown/__test__/test-helpers.ts @@ -15,19 +15,21 @@ 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'], + customObjectVisibility: ['public'], defaultGroupName: 'Miscellaneous', customObjectsGroupName: 'Custom Objects', sortAlphabetically: false, diff --git a/src/core/markdown/adapters/__tests__/interface-adapter.spec.ts b/src/core/markdown/adapters/__tests__/interface-adapter.spec.ts index b69fdd7b..28ac3a5b 100644 --- a/src/core/markdown/adapters/__tests__/interface-adapter.spec.ts +++ b/src/core/markdown/adapters/__tests__/interface-adapter.spec.ts @@ -11,6 +11,7 @@ function linkGenerator(type: string): string { const defaultMarkdownGeneratorConfig: MarkdownGeneratorConfig = { targetDir: '', scope: ['global', 'public'], + customObjectVisibility: ['public'], namespace: '', defaultGroupName: 'Miscellaneous', customObjectsGroupName: 'Custom Objects', 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..dc16aa5e 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,14 @@ export function generateDocs(unparsedBundles: UnparsedSourceBundle[], config: Ma generateForApex(filterApexSourceFiles(unparsedBundles), config), TE.chain((parsedApexFiles) => { return pipe( - reflectCustomFieldsAndObjects(filterCustomObjectsAndFields(unparsedBundles)), + reflectCustomFieldsAndObjectsAndMetadataRecords( + filterCustomObjectsFieldsAndMetadataRecords(unparsedBundles), + config.customObjectVisibility, + ), 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 +79,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 55% rename from src/core/reflection/sobject/reflectCustomFieldsAndObjects.ts rename to src/core/reflection/sobject/reflectCustomFieldsAndObjectsAndMetadataRecords.ts index 083865a4..6bf759a9 100644 --- a/src/core/reflection/sobject/reflectCustomFieldsAndObjects.ts +++ b/src/core/reflection/sobject/reflectCustomFieldsAndObjectsAndMetadataRecords.ts @@ -1,20 +1,39 @@ -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)[], + visibilitiesToDocument: string[], ): TaskEither[]> { function filterNonPublished(parsedFiles: ParsedFile[]): ParsedFile[] { return parsedFiles.filter((parsedFile) => parsedFile.type.deploymentStatus === 'Deployed'); } - function filterNonPublic(parsedFiles: ParsedFile[]): ParsedFile[] { - return parsedFiles.filter((parsedFile) => parsedFile.type.visibility === 'Public'); + /** + * Returns a tuple of parsed objects to document and the names of the objects that should be actively ignored. + * @param parsedFiles + */ + function filter(parsedFiles: ParsedFile[]): [ParsedFile[], string[]] { + function shouldBeDocumented(parsedFile: ParsedFile): boolean { + return visibilitiesToDocument.includes(parsedFile.type.visibility.toLowerCase()); + } + + const objectsToDocument = parsedFiles.filter(shouldBeDocumented); + const objectsToIgnore = parsedFiles + .filter((parsedFile) => !shouldBeDocumented(parsedFile)) + .map((parsedFile) => parsedFile.type.name); + return [objectsToDocument, objectsToIgnore]; } const customObjects = objectBundles.filter( @@ -25,37 +44,52 @@ 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, TE.map(filterNonPublished), - TE.map(filterNonPublic), - TE.bindTo('objects'), + TE.map(filter), + TE.bindTo('filterResult'), TE.bind('fields', () => generateForFields(customFields)), - TE.map(({ objects, fields }) => { - return [...mapFieldsToObjects(objects, fields), ...mapExtensionFields(objects, fields)]; + TE.bind('metadata', () => generateForMetadata(customMetadata)), + TE.map(({ filterResult, fields, metadata }) => { + return [...mapFieldsAndMetadata(filterResult[0], fields, metadata), ...mapExtensionFields(filterResult, 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)], }, }; }); @@ -64,11 +98,16 @@ function mapFieldsToObjects( // "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( - objects: ParsedFile[], + filterResult: [ParsedFile[], string[]], fields: ParsedFile[], ): ParsedFile[] { + const objects = filterResult[0]; + const ignoredObjectNames = filterResult[1]; + const extensionFields = fields.filter( - (field) => !objects.some((object) => object.type.name === field.type.parentName), + (field) => + !objects.some((object) => object.type.name.toLowerCase() === field.type.parentName.toLowerCase()) && + !ignoredObjectNames.map((name) => name.toLowerCase()).includes(field.type.parentName.toLowerCase()), ); // There might be many objects for the same parent name, so we need to group the fields by parent name const extensionFieldsByParent = extensionFields.reduce( @@ -97,6 +136,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..35c6d22b 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'; @@ -19,6 +20,7 @@ export type CliConfigurableMarkdownConfig = { sourceDir: string; targetDir: string; scope: string[]; + customObjectVisibility: string[]; namespace?: string; defaultGroupName: string; customObjectsGroupName: string; @@ -29,7 +31,7 @@ export type CliConfigurableMarkdownConfig = { }; export type UserDefinedMarkdownConfig = { - targetGenerator: 'markdown' /** Glob patterns to exclude files from the documentation. */; + targetGenerator: 'markdown'; excludeTags: string[]; exclude: string[]; } & CliConfigurableMarkdownConfig & @@ -53,13 +55,18 @@ export type UserDefinedChangelogConfig = { targetDir: string; fileName: string; scope: string[]; + customObjectVisibility: string[]; exclude: string[]; skipIfNoChanges: boolean; } & Partial; 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 +83,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 +100,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 +120,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/defaults.ts b/src/defaults.ts index 8510cf7a..73f12d68 100644 --- a/src/defaults.ts +++ b/src/defaults.ts @@ -2,9 +2,15 @@ const commonDefaults = { targetDir: './docs/', }; -export const markdownDefaults = { +const markdownAndChangelogDefaults = { ...commonDefaults, scope: ['global'], + customObjectVisibility: ['public'], + exclude: [], +}; + +export const markdownDefaults = { + ...markdownAndChangelogDefaults, defaultGroupName: 'Miscellaneous', customObjectsGroupName: 'Custom Objects', includeMetadata: false, @@ -12,7 +18,6 @@ export const markdownDefaults = { linkingStrategy: 'relative' as const, referenceGuideTitle: 'Reference Guide', excludeTags: [], - exclude: [], }; export const openApiDefaults = { @@ -24,9 +29,7 @@ export const openApiDefaults = { }; export const changeLogDefaults = { - ...commonDefaults, + ...markdownAndChangelogDefaults, fileName: 'changelog', - scope: ['global'], - exclude: [], skipIfNoChanges: true, }; 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', ); }