├── cmd-loader └── main │ └── default │ ├── lwc │ ├── .eslintrc.json │ ├── cmdDatatable │ │ ├── cmdDatatable.js-meta.xml │ │ ├── cmdDatatable.js │ │ └── cmdDatatable.html │ └── cmdLoader │ │ ├── cmdLoader.js-meta.xml │ │ ├── cmdLoader.html │ │ └── cmdLoader.js │ ├── classes │ ├── CMDConstants.cls-meta.xml │ ├── CMDDeployCallback.cls-meta.xml │ ├── CMDLoaderController.cls-meta.xml │ ├── CMDConnectApiDelegate.cls-meta.xml │ ├── CMDDeployCallbackTest.cls-meta.xml │ ├── CMDLoaderControllerTest.cls-meta.xml │ ├── CMDConnectApiDelegateTest.cls-meta.xml │ ├── CMDMetadataOperationsDelegate.cls-meta.xml │ ├── CMDConnectApiDelegateTest.cls │ ├── CMDConstants.cls │ ├── CMDMetadataOperationsDelegate.cls │ ├── CMDConnectApiDelegate.cls │ ├── CMDDeployCallback.cls │ ├── CMDDeployCallbackTest.cls │ ├── CMDLoaderController.cls │ └── CMDLoaderControllerTest.cls │ ├── tabs │ └── Custom_Metadata_Loader.tab-meta.xml │ ├── staticresources │ ├── PapaParse.resource-meta.xml │ ├── PapaParse.js │ └── PapaParse │ │ └── papaparse.min.js │ └── permissionsets │ └── Custom_Metadata_Loader.permissionset-meta.xml ├── .eslintignore ├── tests ├── README.md ├── csvWithoutLabel.csv ├── csvWithoutDeveloperName.csv ├── csvWithoutTextArea.csv ├── testPkg │ ├── package.xml │ ├── layouts │ │ └── My_Custom_Metadata_Type__mdt-My Custom Metadata Type Layout.layout │ └── objects │ │ └── My_Custom_Metadata_Type__mdt.object ├── csvWithTextArea.csv ├── csvWithWarnings.csv └── csvWithManyRowsFewFields.csv ├── .vscode ├── settings.json ├── extensions.json └── launch.json ├── .prettierignore ├── .prettierrc ├── .forceignore ├── config └── project-scratch-def.json ├── .gitignore ├── CONTRIBUTING.md ├── package.json ├── LICENSE ├── sfdx-project.json ├── .github └── workflows │ ├── pr-develop.yml │ ├── push-main.yml │ └── push-develop.yml └── README.md /cmd-loader/main/default/lwc/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@salesforce/eslint-config-lwc/recommended"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/lwc/**/*.css 2 | **/lwc/**/*.html 3 | **/lwc/**/*.json 4 | **/lwc/**/*.svg 5 | **/lwc/**/*.xml 6 | .sfdx -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # Tests 2 | This folder contains: 3 | - a package to deploy a Custom Metadata Type used for testing 4 | - some CSV files used for testing -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "search.exclude": { 3 | "**/node_modules": true, 4 | "**/bower_components": true, 5 | "**/.sfdx": true 6 | } 7 | } -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "salesforce.salesforcedx-vscode", 4 | "redhat.vscode-xml", 5 | "dbaeumer.vscode-eslint", 6 | "esbenp.prettier-vscode" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # List files or directories below to ignore them when running prettier 2 | # More information: https://prettier.io/docs/en/ignore.html 3 | # 4 | 5 | .localdevserver 6 | .sfdx 7 | 8 | coverage/ -------------------------------------------------------------------------------- /cmd-loader/main/default/classes/CMDConstants.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 50.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /cmd-loader/main/default/classes/CMDDeployCallback.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 50.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /cmd-loader/main/default/classes/CMDLoaderController.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 50.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /cmd-loader/main/default/classes/CMDConnectApiDelegate.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 50.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /cmd-loader/main/default/classes/CMDDeployCallbackTest.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 50.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /cmd-loader/main/default/classes/CMDLoaderControllerTest.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 50.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /cmd-loader/main/default/classes/CMDConnectApiDelegateTest.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 50.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /cmd-loader/main/default/classes/CMDMetadataOperationsDelegate.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 50.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /cmd-loader/main/default/lwc/cmdDatatable/cmdDatatable.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 50.0 4 | false 5 | 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "overrides": [ 4 | { 5 | "files": "**/lwc/**/*.html", 6 | "options": { "parser": "lwc" } 7 | }, 8 | { 9 | "files": "*.{cmp,page,component}", 10 | "options": { "parser": "html" } 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /cmd-loader/main/default/tabs/Custom_Metadata_Loader.tab-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | cmdLoader 5 | Custom44: Hammer 6 | 7 | -------------------------------------------------------------------------------- /cmd-loader/main/default/classes/CMDConnectApiDelegateTest.cls: -------------------------------------------------------------------------------- 1 | @IsTest 2 | private class CMDConnectApiDelegateTest { 3 | @IsTest 4 | static void getTodayFeedItemsReturnsCorrectly() { 5 | List res = new CMDConnectApiDelegate().getTodayFeedItems(UserInfo.getUserId()); 6 | System.assertNotEquals(null, res); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /cmd-loader/main/default/lwc/cmdLoader/cmdLoader.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 50.0 4 | true 5 | 6 | lightning__Tab 7 | 8 | 9 | -------------------------------------------------------------------------------- /cmd-loader/main/default/staticresources/PapaParse.resource-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Private 4 | application/zip 5 | https://github.com/mholt/PapaParse 6 | 7 | -------------------------------------------------------------------------------- /.forceignore: -------------------------------------------------------------------------------- 1 | # List files or directories below to ignore them when running force:source:push, force:source:pull, and force:source:status 2 | # More information: https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_exclude_source.htm 3 | # 4 | 5 | package.xml 6 | 7 | # LWC configuration files 8 | **/jsconfig.json 9 | **/.eslintrc.json 10 | 11 | # LWC Jest 12 | **/__tests__/** -------------------------------------------------------------------------------- /config/project-scratch-def.json: -------------------------------------------------------------------------------- 1 | { 2 | "orgName": "CMDLoaderLwc", 3 | "edition": "Developer", 4 | "features": ["AuthorApex"], 5 | "language": "en_US", 6 | "settings": { 7 | "lightningExperienceSettings": { 8 | "enableS1DesktopEnabled": true 9 | }, 10 | "securitySettings": { 11 | "sessionSettings": { 12 | "forceRelogin": false 13 | } 14 | }, 15 | "mobileSettings": { 16 | "enableS1EncryptedStoragePref2": false 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch Apex Replay Debugger", 9 | "type": "apex-replay", 10 | "request": "launch", 11 | "logFile": "${command:AskForLogFileName}", 12 | "stopOnEntry": true, 13 | "trace": true 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /cmd-loader/main/default/permissionsets/Custom_Metadata_Loader.permissionset-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CMDLoaderController 5 | true 6 | 7 | false 8 | 9 | Salesforce 10 | 11 | Custom_Metadata_Loader 12 | Visible 13 | 14 | 15 | -------------------------------------------------------------------------------- /cmd-loader/main/default/classes/CMDConstants.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Marco Zeuli 3 | * Licensed under MIT license. 4 | * For full license text, see LICENSE file in the repo root or https://opensource.org/licenses/MIT 5 | * If you would like to contribute https://github.com/maaaaarco/spaghetti-cmd-loader 6 | */ 7 | @NamespaceAccessible 8 | public inherited sharing class CMDConstants { 9 | public static final String MDT_SUFFIX = '__mdt'; 10 | public static final Set MASTER_LABEL_FIELD_NAMES = new Set{ 11 | 'MasterLabel', 12 | 'Label' 13 | }; 14 | public static final String DEVELOPER_NAME_FIELD_NAME = 'DeveloperName'; 15 | } 16 | -------------------------------------------------------------------------------- /cmd-loader/main/default/classes/CMDMetadataOperationsDelegate.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Marco Zeuli 3 | * Licensed under MIT license. 4 | * For full license text, see LICENSE file in the repo root or https://opensource.org/licenses/MIT 5 | * If you would like to contribute https://github.com/maaaaarco/spaghetti-cmd-loader 6 | */ 7 | 8 | /** 9 | * This Delegate allows for testing with mocks 10 | */ 11 | public inherited sharing class CMDMetadataOperationsDelegate { 12 | public Id enqueueDeployment(Metadata.DeployContainer container, Metadata.DeployCallback callback) { 13 | return Metadata.Operations.enqueueDeployment(container, callback); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # This file is used for Git repositories to specify intentionally untracked files that Git should ignore. 2 | # If you are not using git, you can delete this file. For more information see: https://git-scm.com/docs/gitignore 3 | # For useful gitignore templates see: https://github.com/github/gitignore 4 | 5 | # Salesforce cache 6 | .sfdx/ 7 | .localdevserver/ 8 | 9 | # LWC VSCode autocomplete 10 | **/lwc/jsconfig.json 11 | 12 | # LWC Jest coverage reports 13 | coverage/ 14 | 15 | # Logs 16 | logs 17 | *.log 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | 22 | # Dependency directories 23 | node_modules/ 24 | 25 | # Eslint cache 26 | .eslintcache 27 | 28 | # MacOS system files 29 | .DS_Store 30 | **/__MACOSX/** 31 | 32 | # Windows system files 33 | Thumbs.db 34 | ehthumbs.db 35 | [Dd]esktop.ini 36 | $RECYCLE.BIN/ 37 | 38 | tmp/ 39 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | 1. Familiarize yourself with the codebase 4 | 1. Create a new issue before starting your project so that we can keep track of 5 | what you are trying to add/fix. 6 | 1. Fork this repository. 7 | 1. Edit the code in your fork. 8 | 1. Send us a pull request when you are done. We'll review your code, suggest any 9 | needed changes, and merge it in. 10 | 11 | ## Branches 12 | 13 | - We work in `develop`. 14 | - Every push on `develop` will create a new package version 15 | - We release from `master`. 16 | - Our work happens in _topic_ branches (feature and/or bug-fix). 17 | - feature as well as bug-fix branches are based on `develop` 18 | 19 | 20 | ### Merging `develop` into `master` 21 | 22 | - When a development cycle finishes, the content of the `develop` is merged into `master` branch. 23 | - The latest package version is then promoted to `released` -------------------------------------------------------------------------------- /tests/csvWithoutLabel.csv: -------------------------------------------------------------------------------- 1 | "DeveloperName","spaghettiCMD__CheckboxField__c","spaghettiCMD__DateField__c","spaghettiCMD__DateTimeField__c","spaghettiCMD__LongField__c","spaghettiCMD__IntegerField__c","spaghettiCMD__DecimalField__c","spaghettiCMD__PercentageField__c","spaghettiCMD__PhoneField__c","spaghettiCMD__PicklistField__c","spaghettiCMD__TextField__c","spaghettiCMD__UrlField__c" 2 | "Record_1","true","2020-06-10","2020-06-10 19:23:00","123456789009876540","123456","123456.654","99.99","+491234567890","Value 1","this is a text field","https://developer.salesforce.com/" 3 | "Record_3","true","2020-06-10","2020-06-10 19:23:00","123456789009876540","123456","123456.654","99.99","+491234567890","Value 1","this is a text field","https://developer.salesforce.com/" 4 | "Record_2","true","2020-06-10","2020-06-10 19:23:00","123456789009876540","123456","123456.654","99.99","+491234567890","Value 1","this is a text field","https://developer.salesforce.com/" -------------------------------------------------------------------------------- /tests/csvWithoutDeveloperName.csv: -------------------------------------------------------------------------------- 1 | "MasterLabel","spaghettiCMD__CheckboxField__c","spaghettiCMD__DateField__c","spaghettiCMD__DateTimeField__c","spaghettiCMD__LongField__c","spaghettiCMD__IntegerField__c","spaghettiCMD__DecimalField__c","spaghettiCMD__PercentageField__c","spaghettiCMD__PhoneField__c","spaghettiCMD__PicklistField__c","spaghettiCMD__TextField__c","spaghettiCMD__UrlField__c" 2 | "Record 1","true","2020-06-10","2020-06-10 19:23:00","123456789009876540","123456","123456.654","99.99","+491234567890","Value 1","this is a text field","https://developer.salesforce.com/" 3 | "Record 3","true","2020-06-10","2020-06-10 19:23:00","123456789009876540","123456","123456.654","99.99","+491234567890","Value 1","this is a text field","https://developer.salesforce.com/" 4 | "Record 2","true","2020-06-10","2020-06-10 19:23:00","123456789009876540","123456","123456.654","99.99","+491234567890","Value 1","this is a text field","https://developer.salesforce.com/" -------------------------------------------------------------------------------- /tests/csvWithoutTextArea.csv: -------------------------------------------------------------------------------- 1 | "DeveloperName","MasterLabel","spaghettiCMD__CheckboxField__c","spaghettiCMD__DateField__c","spaghettiCMD__DateTimeField__c","spaghettiCMD__LongField__c","spaghettiCMD__IntegerField__c","spaghettiCMD__DecimalField__c","spaghettiCMD__PercentageField__c","spaghettiCMD__PhoneField__c","spaghettiCMD__PicklistField__c","spaghettiCMD__TextField__c","spaghettiCMD__UrlField__c" 2 | "Record_1","Record 1","true","2020-06-10","2020-06-10 19:23:00","123456789009876540","123456","123456.654","99.99","+491234567890","Value 1","this is a text field","https://developer.salesforce.com/" 3 | "Record_3","Record 3","true","2020-06-10","2020-06-10 19:23:00","123456789009876540","123456","123456.654","99.99","+491234567890","Value 1","this is a text field","https://developer.salesforce.com/" 4 | "Record_2","Record 2","true","2020-06-10","2020-06-10 19:23:00","123456789009876540","123456","123456.654","99.99","+491234567890","Value 1","this is a text field","https://developer.salesforce.com/" -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "salesforce-app", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "Salesforce App", 6 | "scripts": { 7 | "lint": "npm run lint:lwc", 8 | "lint:lwc": "eslint force-app/main/default/lwc", 9 | "test": "npm run test:unit", 10 | "test:unit": "sfdx-lwc-jest", 11 | "test:unit:watch": "sfdx-lwc-jest --watch", 12 | "test:unit:debug": "sfdx-lwc-jest --debug", 13 | "test:unit:coverage": "sfdx-lwc-jest --coverage", 14 | "prettier": "prettier --write \"**/*.{cls,cmp,component,css,html,js,json,md,page,trigger,xml,yaml,yml}\"", 15 | "prettier:verify": "prettier --list-different \"**/*.{cls,cmp,component,css,html,js,json,md,page,trigger,xml,yaml,yml}\"" 16 | }, 17 | "devDependencies": { 18 | "@prettier/plugin-xml": "^0.7.0", 19 | "@salesforce/eslint-config-lwc": "^0.4.0", 20 | "@salesforce/sfdx-lwc-jest": "^0.7.0", 21 | "eslint": "^5.16.0", 22 | "prettier": "1.19.1", 23 | "prettier-plugin-apex": "1.5.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /cmd-loader/main/default/lwc/cmdDatatable/cmdDatatable.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Marco Zeuli 3 | * Licensed under MIT license. 4 | * For full license text, see LICENSE file in the repo root or https://opensource.org/licenses/MIT 5 | * If you would like to contribute https://github.com/maaaaarco/spaghetti-cmd-loader 6 | */ 7 | 8 | /** 9 | * Custom datatable component, used to display big CSV files 10 | */ 11 | import { LightningElement, api } from "lwc"; 12 | 13 | export default class CmdDatatable extends LightningElement { 14 | @api columns; 15 | @api records; 16 | 17 | get headers() { 18 | return this.columns.map(column => { 19 | return column.fieldName; 20 | }); 21 | } 22 | 23 | get rows() { 24 | const headers = this.headers; 25 | return this.records.map((record, idx) => { 26 | const cells = []; 27 | headers.forEach(header => { 28 | cells.push(record[header]); 29 | }); 30 | return { 31 | cells, 32 | id: idx 33 | }; 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Marco Zeuli 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cmd-loader/main/default/lwc/cmdDatatable/cmdDatatable.html: -------------------------------------------------------------------------------- 1 | 7 | 35 | -------------------------------------------------------------------------------- /cmd-loader/main/default/classes/CMDConnectApiDelegate.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Marco Zeuli 3 | * Licensed under MIT license. 4 | * For full license text, see LICENSE file in the repo root or https://opensource.org/licenses/MIT 5 | * If you would like to contribute https://github.com/maaaaarco/spaghetti-cmd-loader 6 | */ 7 | 8 | /** 9 | * ConnectApi methods are not supported in data siloed tests. 10 | * This Delegate allows for testing without using the SeeAllData=true 11 | */ 12 | public inherited sharing class CMDConnectApiDelegate { 13 | /** 14 | * Creates new FeedItem 15 | * @return Created FeedItem 16 | */ 17 | public ConnectApi.FeedElement postFeedElement( 18 | Id networkId, 19 | ConnectApi.FeedItemInput feed 20 | ) { 21 | return ConnectApi.ChatterFeeds.postFeedElement(networkId, feed); 22 | } 23 | 24 | /** 25 | * Retrieves FeedItem records created in the current day 26 | * @param parentId Parent Id of FeedItem records 27 | * @return A list of FeedItem or an empty list 28 | */ 29 | public List getTodayFeedItems(Id parentId){ 30 | return [ 31 | SELECT Body 32 | FROM FeedItem 33 | WHERE ParentId = :UserInfo.getUserId() AND CreatedDate = TODAY 34 | LIMIT 1000 35 | ]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/testPkg/package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Layout 5 | My_Custom_Metadata_Type__mdt-My Custom Metadata Type Layout 6 | 7 | 8 | CustomObject 9 | My_Custom_Metadata_Type__mdt 10 | 11 | 12 | CustomField 13 | My_Custom_Metadata_Type__mdt.CheckboxField__c 14 | My_Custom_Metadata_Type__mdt.DateField__c 15 | My_Custom_Metadata_Type__mdt.DateTimeField__c 16 | My_Custom_Metadata_Type__mdt.DecimalField__c 17 | My_Custom_Metadata_Type__mdt.IntegerField__c 18 | My_Custom_Metadata_Type__mdt.LongField__c 19 | My_Custom_Metadata_Type__mdt.PercentageField__c 20 | My_Custom_Metadata_Type__mdt.PhoneField__c 21 | My_Custom_Metadata_Type__mdt.PicklistField__c 22 | My_Custom_Metadata_Type__mdt.TextAreaField__c 23 | My_Custom_Metadata_Type__mdt.TextAreaLong__c 24 | My_Custom_Metadata_Type__mdt.TextField__c 25 | My_Custom_Metadata_Type__mdt.UrlField__c 26 | 27 | 48.0 28 | -------------------------------------------------------------------------------- /tests/csvWithTextArea.csv: -------------------------------------------------------------------------------- 1 | "DeveloperName","MasterLabel","spaghettiCMD__CheckboxField__c","spaghettiCMD__DateField__c","spaghettiCMD__DateTimeField__c","spaghettiCMD__LongField__c","spaghettiCMD__IntegerField__c","spaghettiCMD__DecimalField__c","spaghettiCMD__PercentageField__c","spaghettiCMD__PhoneField__c","spaghettiCMD__PicklistField__c","spaghettiCMD__TextField__c","spaghettiCMD__TextAreaField__c","spaghettiCMD__TextAreaLong__c","spaghettiCMD__UrlField__c" 2 | "Record_1","Record 1","true","2020-06-10","2020-06-10 19:23:00","123456789009876540","123456","123456.654","99.99","+491234567890","Value 1","this is a text field","text area line 1 3 | text area line 2","text area long line 1 4 | text area long line 2 5 | text area long line 3 6 | text area long line 4","https://developer.salesforce.com/" 7 | "Record_3","Record 3","true","2020-06-10","2020-06-10 19:23:00","123456789009876540","123456","123456.654","99.99","+491234567890","Value 1","this is a text field","text area line 1 8 | text area line 2","text area long line 1 9 | text area long line 2 10 | text area long line 3 11 | text area long line 4","https://developer.salesforce.com/" 12 | "Record_2","Record 2","true","2020-06-10","2020-06-10 19:23:00","123456789009876540","123456","123456.654","99.99","+491234567890","Value 1","this is a text field","text area line 1 13 | text area line 2","text area long line 1 14 | text area long line 2 15 | text area long line 3 16 | text area long line 4","https://developer.salesforce.com/" -------------------------------------------------------------------------------- /sfdx-project.json: -------------------------------------------------------------------------------- 1 | { 2 | "namespace": "spaghettiCMD", 3 | "sfdcLoginUrl": "https://login.salesforce.com", 4 | "sourceApiVersion": "50.0", 5 | "packageDirectories": [ 6 | { 7 | "path": "cmd-loader", 8 | "default": true, 9 | "package": "Spaghetti CMD", 10 | "versionName": "ver 1.3", 11 | "versionNumber": "1.3.1.NEXT" 12 | } 13 | ], 14 | "packageAliases": { 15 | "Spaghetti CMD": "0Ho1t000000wk4FCAQ", 16 | "Spaghetti CMD@0.5.0-1": "04t1t000002hzNuAAI", 17 | "Spaghetti CMD@1.0.0-4": "04t1t000002hzYUAAY", 18 | "Spaghetti CMD@1.0.0-5": "04t1t000002hzYjAAI", 19 | "Spaghetti CMD@1.0.0-6": "04t1t000002hzYtAAI", 20 | "Spaghetti CMD@1.0.0-7": "04t1t000002hzYyAAI", 21 | "Spaghetti CMD@1.0.0-8": "04t1t000002hzZDAAY", 22 | "Spaghetti CMD@1.0.0-10": "04t1t000002hzZwAAI", 23 | "Spaghetti CMD@1.1.0-1": "04t1t000002hza1AAA", 24 | "Spaghetti CMD@1.1.1-1": "04t1t000003TtsnAAC", 25 | "Spaghetti CMD@1.1.1-5": "04t1t000003Ttt7AAC", 26 | "Spaghetti CMD@1.1.1-6": "04t1t000003TttgAAC", 27 | "Spaghetti CMD@1.1.1-8": "04t1t000003TtxyAAC", 28 | "Spaghetti CMD@1.1.1-10": "04t1t000003Tty8AAC", 29 | "Spaghetti CMD@1.2.1-1": "04t1t000003HUFbAAO", 30 | "Spaghetti CMD@1.2.1-4": "04t1t000003LfY4AAK", 31 | "Spaghetti CMD@1.2.1-5": "04t1t000003LfY9AAK", 32 | "Spaghetti CMD@1.3.1-1": "04t1t000003LfYEAA0" 33 | } 34 | } -------------------------------------------------------------------------------- /.github/workflows/pr-develop.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request to Develop 2 | 3 | on: 4 | pull_request: 5 | branches: [ develop ] 6 | 7 | jobs: 8 | validate_pull_request: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - name: Install Salesforce CLI 15 | run: | 16 | wget https://developer.salesforce.com/media/salesforce-cli/sfdx-linux-amd64.tar.xz 17 | mkdir sfdx-cli 18 | tar xJf sfdx-linux-amd64.tar.xz -C sfdx-cli --strip-components 1 19 | ./sfdx-cli/install 20 | 21 | - name: Install jq 22 | run: | 23 | sudo apt-get install jq 24 | 25 | - name: Populate auth file 26 | shell: bash 27 | run: 'echo ${{secrets.DEVHUB_SFDX_URL}} > ./DEVHUB_SFDX_URL.txt' 28 | 29 | - name: Authenticate Dev Hub 30 | run: 'sfdx force:auth:sfdxurl:store -f ./DEVHUB_SFDX_URL.txt -a devhub -d' 31 | 32 | - name: Create scratch org 33 | run: 'sfdx force:org:create -f config/project-scratch-def.json -a ci_scratch -s -d 1' 34 | 35 | - name: Push source to scratch org 36 | run: 'sfdx force:source:push' 37 | 38 | - name: Check code coverage 39 | run: | 40 | sfdx force:apex:test:run --codecoverage --resultformat json --synchronous --testlevel RunLocalTests --wait 10 > tests.json 41 | coverage=$(jq .result.summary.orgWideCoverage tests.json | grep -Eo "[[:digit:]]+") 42 | test $coverage -ge 75 43 | 44 | - name: Delete scratch org 45 | if: always() 46 | run: 'sfdx force:org:delete -p -u ci_scratch' 47 | -------------------------------------------------------------------------------- /.github/workflows/push-main.yml: -------------------------------------------------------------------------------- 1 | name: Publish new release 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | jobs: 8 | publish_release: 9 | # The type of runner that the job will run on 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Install Salesforce CLI 16 | run: | 17 | wget https://developer.salesforce.com/media/salesforce-cli/sfdx-linux-amd64.tar.xz 18 | mkdir sfdx-cli 19 | tar xJf sfdx-linux-amd64.tar.xz -C sfdx-cli --strip-components 1 20 | ./sfdx-cli/install 21 | 22 | - name: Install jq 23 | run: | 24 | sudo apt-get install jq 25 | 26 | - name: Populate auth file 27 | shell: bash 28 | run: 'echo ${{secrets.DEVHUB_SFDX_URL}} > ./DEVHUB_SFDX_URL.txt' 29 | 30 | - name: Authenticate Dev Hub 31 | run: 'sfdx force:auth:sfdxurl:store -f ./DEVHUB_SFDX_URL.txt -a devhub -d' 32 | 33 | - name: Promote latest version 34 | run: | 35 | version_id=$(grep -o "04t[[:alnum:]]\{15\}" sfdx-project.json | tail -n1) 36 | sfdx force:package:version:promote -p "$version_id" --noprompt 37 | 38 | - name: Tag new release 39 | run: | 40 | tag_name=$(jq ".packageDirectories[0].versionName" sfdx-project.json | tr -d '"'| tr -d ' ') 41 | pkg_name=$(jq ".packageDirectories[0].package" sfdx-project.json | tr -d '"') 42 | git config user.name "release[bot]" 43 | git config user.email "<>" 44 | git tag -a "$tag_name" -m "$pkg_name $tag_name" 45 | git push origin "$tag_name" -------------------------------------------------------------------------------- /.github/workflows/push-develop.yml: -------------------------------------------------------------------------------- 1 | name: Create pre-release version 2 | 3 | on: 4 | push: 5 | branches: [ develop ] 6 | 7 | jobs: 8 | pre_release: 9 | # The type of runner that the job will run on 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Install Salesforce CLI 16 | run: | 17 | wget https://developer.salesforce.com/media/salesforce-cli/sfdx-linux-amd64.tar.xz 18 | mkdir sfdx-cli 19 | tar xJf sfdx-linux-amd64.tar.xz -C sfdx-cli --strip-components 1 20 | ./sfdx-cli/install 21 | 22 | - name: Populate auth file 23 | shell: bash 24 | run: 'echo ${{secrets.DEVHUB_SFDX_URL}} > ./DEVHUB_SFDX_URL.txt' 25 | 26 | - name: Authenticate Dev Hub 27 | run: 'sfdx force:auth:sfdxurl:store -f ./DEVHUB_SFDX_URL.txt -a devhub -d' 28 | 29 | - name: Create new version 30 | run: | 31 | sfdx force:package:version:create -x -p "Spaghetti CMD" -w 60 --codecoverage 32 | new_version_id=$(grep -o "04t[[:alnum:]]\{15\}" sfdx-project.json | tail -n1) 33 | echo "version_id=${new_version_id}" >> $GITHUB_ENV 34 | 35 | - name: Check code coverage 36 | run: | 37 | test $(sfdx force:package:version:report -p "$version_id" --json | jq .result.HasPassedCodeCoverageCheck) = 'true' 38 | 39 | - name: Install new version in Dev Hub 40 | run: | 41 | sfdx force:package:install -p "$version_id" -u devhub --wait 10 --publishwait 10 42 | 43 | - name: Store new version id 44 | run: | 45 | sed -i -e "s/04t[[:alnum:]]\{15\}/${version_id}/" README.md 46 | git config user.name "release[bot]" 47 | git config user.email "<>" 48 | git add README.md 49 | git add sfdx-project.json 50 | git commit -m "Updating new pre-release version" 51 | git push -------------------------------------------------------------------------------- /cmd-loader/main/default/classes/CMDDeployCallback.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Marco Zeuli 3 | * Licensed under MIT license. 4 | * For full license text, see LICENSE file in the repo root or https://opensource.org/licenses/MIT 5 | * If you would like to contribute https://github.com/maaaaarco/spaghetti-cmd-loader 6 | */ 7 | 8 | /** 9 | * Invoked after a Metadata Api deployment completes (successfully or not), post the result on the 10 | * user (who triggered the deployment) chatter feed 11 | */ 12 | @NamespaceAccessible 13 | public inherited sharing class CMDDeployCallback implements Metadata.DeployCallback { 14 | @TestVisible 15 | static final Integer MAX_FAILURES = 20; 16 | 17 | Metadata.DeployResult result; 18 | 19 | @TestVisible 20 | private CMDConnectApiDelegate feedApi; 21 | 22 | public CMDDeployCallback() { 23 | this.feedApi = new CMDConnectApiDelegate(); 24 | } 25 | 26 | public void handleResult( 27 | Metadata.DeployResult result, 28 | Metadata.DeployCallbackContext context 29 | ) { 30 | if (!result.done) { 31 | return; 32 | } 33 | 34 | this.result = result; 35 | 36 | ConnectApi.FeedItemInput feedItemInput = new ConnectApi.FeedItemInput(); 37 | ConnectApi.MessageBodyInput messageBodyInput = new ConnectApi.MessageBodyInput(); 38 | ConnectApi.TextSegmentInput textSegmentInput = new ConnectApi.TextSegmentInput(); 39 | messageBodyInput.messageSegments = new List(); 40 | 41 | textSegmentInput.text = parseDeploymentStatus(); 42 | 43 | if (!result.success) { 44 | textSegmentInput.text += parseErrors(); 45 | } 46 | 47 | messageBodyInput.messageSegments.add(textSegmentInput); 48 | 49 | feedItemInput.body = messageBodyInput; 50 | feedItemInput.feedElementType = ConnectApi.FeedElementType.FeedItem; 51 | feedItemInput.subjectId = result.createdBy; 52 | 53 | feedApi.postFeedElement(Network.getNetworkId(), feedItemInput); 54 | } 55 | 56 | private String parseDeploymentStatus() { 57 | String numErrors = this.result.numberComponentErrors != null 58 | ? String.valueOf(this.result.numberComponentErrors) 59 | : '0'; 60 | 61 | // if there's at least one error then none of the components get deployed 62 | String numDeployed = numErrors != '0' 63 | ? '0' 64 | : this.result.numberComponentsDeployed != null 65 | ? String.valueOf(this.result.numberComponentsDeployed) 66 | : '0'; 67 | return String.format( 68 | '[{0}] - Deployment done, status: {1}\nComponents deployed: {2}\nErrors: {3}\n', 69 | new List{ 70 | this.result.id, 71 | this.result.status.name(), 72 | numDeployed, 73 | numErrors 74 | } 75 | ); 76 | } 77 | 78 | private String parseErrors() { 79 | String res = ''; 80 | 81 | // 1. parse the general error message 82 | if (String.isNotBlank(this.result.errorMessage)) { 83 | res += String.format( 84 | '[ {0} ] - {1}\n', 85 | new List{ 86 | this.result.errorStatusCode.name(), 87 | this.result.errorMessage 88 | } 89 | ); 90 | } 91 | 92 | // 2. checks for error details 93 | if ( 94 | this.result.details != null && 95 | this.result.details.componentFailures != null 96 | ) { 97 | for ( 98 | Integer i = 0; 99 | i < MAX_FAILURES && 100 | i < this.result.details.componentFailures.size(); 101 | i++ 102 | ) { 103 | Metadata.DeployMessage msg = this.result.details.componentFailures[i]; 104 | res += String.format( 105 | '{0} : {1} - {2}\n', 106 | new List{ 107 | msg.fullName, 108 | msg.lineNumber != null ? String.valueOf(msg.lineNumber) : '0', 109 | msg.problem 110 | } 111 | ); 112 | } 113 | 114 | // are there any more ? 115 | if (this.result.details.componentFailures.size() > MAX_FAILURES) { 116 | res += String.format( 117 | '...{0} more, go to "Setup -> Deployment Status" for additional details', 118 | new List{ 119 | String.valueOf( 120 | this.result.details.componentFailures.size() - MAX_FAILURES 121 | ) 122 | } 123 | ); 124 | } 125 | } 126 | 127 | return res; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /tests/testPkg/layouts/My_Custom_Metadata_Type__mdt-My Custom Metadata Type Layout.layout: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | false 6 | true 7 | 8 | 9 | 10 | Required 11 | MasterLabel 12 | 13 | 14 | Required 15 | DeveloperName 16 | 17 | 18 | Edit 19 | CheckboxField__c 20 | 21 | 22 | Edit 23 | DateField__c 24 | 25 | 26 | Edit 27 | DateTimeField__c 28 | 29 | 30 | Edit 31 | LongField__c 32 | 33 | 34 | Edit 35 | IntegerField__c 36 | 37 | 38 | Edit 39 | DecimalField__c 40 | 41 | 42 | Edit 43 | PercentageField__c 44 | 45 | 46 | Edit 47 | PhoneField__c 48 | 49 | 50 | Edit 51 | PicklistField__c 52 | 53 | 54 | Edit 55 | TextField__c 56 | 57 | 58 | Edit 59 | TextAreaField__c 60 | 61 | 62 | Edit 63 | TextAreaLong__c 64 | 65 | 66 | Edit 67 | UrlField__c 68 | 69 | 70 | 71 | 72 | Edit 73 | IsProtected 74 | 75 | 76 | Required 77 | NamespacePrefix 78 | 79 | 80 | 81 | 82 | 83 | false 84 | false 85 | true 86 | 87 | 88 | 89 | Readonly 90 | CreatedById 91 | 92 | 93 | 94 | 95 | Readonly 96 | LastModifiedById 97 | 98 | 99 | 100 | 101 | 102 | false 103 | false 104 | false 105 | 106 | 107 | 108 | false 109 | false 110 | false 111 | false 112 | false 113 | 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![release](https://img.shields.io/badge/release-Winter_'21-g) 2 | 3 | # Custom Metadata Loader 4 | Create or Update Custom Metadata Type records from CSV file. [Read more](https://spaghetti.dev/Unlocked-packages-and-GitHub-actions/) about this project. 5 | 6 | - [Installation options](#installation) 7 | - [Url](#installation-url) 8 | - [Unlocked package](#installation-unlocked-pkg) 9 | - [Manual](#installation-clone-repo) 10 | - [User guide](#user-guide) 11 | - [Supported field types](#user-guide-fields) 12 | - [CSV columns](#user-guide-csv-cols) 13 | - [Limitations](#user-guide-limitations) 14 | - [Demo](#demo) 15 | - [Contributing](#contributing) 16 | - [Credits](#credits) 17 | 18 | # Installation options 19 | 20 | Deploy to Salesforce 22 | 23 | 24 | ## URL (recommended) 25 | - [Production/Developer](https://login.salesforce.com/packaging/installPackage.apexp?p0=04t1t000003LfYEAA0) 26 | - [Sandbox](https://test.salesforce.com/packaging/installPackage.apexp?p0=04t1t000003LfYEAA0) 27 | 28 | This approach is recommended since all components will be deployed using a namespace, removing the chance of failures due to conflicting API names. 29 | 30 | ## Unlocked package (recommended) 31 | You can install this as an Unlocked Package, using the CLI, by running the following command: 32 | ```bash 33 | sfdx force:package:install --package "04t1t000003LfYEAA0" --targetusername YOUR_ORG_ALIAS --wait 10 --publishwait 10 34 | ``` 35 | This approach is recommended since all components will be deployed using a namespace, removing the chance of failures due to conflicting API names. 36 | 37 | ## Manual 38 | You can install this by cloning the repository and deploying the content of _cmd-loader_ folder. Before that you should remove the _namespace_ property in the _sfdx-project.json_ file. 39 | ```json 40 | "namespace": "spaghettiCMD" 41 | ``` 42 | 43 | # User Guide 44 | After deploying the application follow these step to enable it for your users: 45 | 46 | 1. Assign yourself, or ask your System Administrator to assign, the _Custom Metadata Loader_ permission set to your user 47 | 1. In the App Launcher search for _Custom Metadata Loader_ tab 48 | 1. Select the CSV file 49 | 1. Select the Custom Metadata Type 50 | 1. Click on _Load Records_ button 51 | 52 | ## Field types supported 53 | - Checkbox 54 | - Date, the specified string should use the standard date format “yyyy-MM-dd”. 55 | - Datetime, the specified string should use the standard date format “yyyy-MM-dd HH:mm:ss” in the local time zone. 56 | - Email 57 | - Number 58 | - Percent 59 | - Phone 60 | - Picklist 61 | - Text 62 | - Text Area, supports multiline text 63 | - Text Area Long, supports multiline text 64 | - URL 65 | 66 | ## CSV columns 67 | CSV columns must match the API name on your Custom Metadata Type fields. The CSV file must include these columns: 68 | - _DeveloperName_, is the unique identifier for the record 69 | - One between _MasterLabel_ or _Label_, for the record's label 70 | 71 | ## Limitations 72 | This application does not impose any hard limit on the CSV file size or number of rows but it is subjected to all [Apex Governor Limit](https://developer.salesforce.com/docs/atlas.en-us.salesforce_app_limits_cheatsheet.meta/salesforce_app_limits_cheatsheet/salesforce_app_limits_platform_apexgov.htm). 73 | 74 | If your CSV file contains more than 250 rows the application will automatically split it into smaller chunks of 250 rows each. Chunks will be loaded sequentially. 75 | 76 | For more complex use cases consider that the [Salesforce CLI](https://developer.salesforce.com/docs/atlas.en-us.sfdx_cli_reference.meta/sfdx_cli_reference/cli_reference.htm) has a bunch of commands to work with Custom Metadata Types. Check them out [here](https://developer.salesforce.com/docs/atlas.en-us.sfdx_cli_reference.meta/sfdx_cli_reference/cli_reference_force_cmdt.htm#cli_reference_force_cmdt). 77 | 78 | # Demo 79 | YouTube video: 80 | 81 | [![Demo Video](https://img.youtube.com/vi/abYr7B-5vsA/0.jpg)](https://www.youtube.com/watch?v=abYr7B-5vsA) 82 | 83 | # Contribute 84 | If you are interested in contributing, please take a look at the [CONTRIBUTING](CONTRIBUTING.md) guide. 85 | 86 | # Credits 87 | - [Papa Parse](https://www.papaparse.com/) for its amazing Javascript CSV parser -------------------------------------------------------------------------------- /tests/testPkg/objects/My_Custom_Metadata_Type__mdt.object: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | My Custom Metadata Types 5 | Public 6 | 7 | CheckboxField__c 8 | false 9 | false 10 | DeveloperControlled 11 | 12 | Checkbox 13 | 14 | 15 | DateField__c 16 | false 17 | DeveloperControlled 18 | 19 | false 20 | Date 21 | 22 | 23 | DateTimeField__c 24 | false 25 | DeveloperControlled 26 | 27 | false 28 | DateTime 29 | 30 | 31 | DecimalField__c 32 | false 33 | DeveloperControlled 34 | 35 | 13 36 | false 37 | 3 38 | Number 39 | false 40 | 41 | 42 | IntegerField__c 43 | false 44 | DeveloperControlled 45 | 46 | 6 47 | false 48 | 0 49 | Number 50 | false 51 | 52 | 53 | LongField__c 54 | false 55 | DeveloperControlled 56 | 57 | 18 58 | false 59 | 0 60 | Number 61 | false 62 | 63 | 64 | PercentageField__c 65 | false 66 | DeveloperControlled 67 | 68 | 18 69 | false 70 | 2 71 | Percent 72 | 73 | 74 | PhoneField__c 75 | false 76 | DeveloperControlled 77 | 78 | false 79 | Phone 80 | 81 | 82 | PicklistField__c 83 | false 84 | DeveloperControlled 85 | 86 | false 87 | Picklist 88 | 89 | true 90 | 91 | false 92 | 93 | Value 1 94 | true 95 | 96 | 97 | 98 | Value 2 99 | false 100 | 101 | 102 | 103 | 104 | 105 | 106 | TextAreaField__c 107 | false 108 | DeveloperControlled 109 | 110 | false 111 | TextArea 112 | 113 | 114 | TextAreaLong__c 115 | false 116 | DeveloperControlled 117 | 118 | 32768 119 | LongTextArea 120 | 3 121 | 122 | 123 | TextField__c 124 | false 125 | DeveloperControlled 126 | 127 | 255 128 | false 129 | Text 130 | false 131 | 132 | 133 | UrlField__c 134 | false 135 | DeveloperControlled 136 | 137 | false 138 | Url 139 | 140 | 141 | -------------------------------------------------------------------------------- /cmd-loader/main/default/classes/CMDDeployCallbackTest.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Marco Zeuli 3 | * Licensed under MIT license. 4 | * For full license text, see LICENSE file in the repo root or https://opensource.org/licenses/MIT 5 | * If you would like to contribute https://github.com/maaaaarco/spaghetti-cmd-loader 6 | */ 7 | @IsTest 8 | private class CMDDeployCallbackTest { 9 | @IsTest 10 | static void handleResultDoesNothingIfDeployIsNotCompleted() { 11 | Metadata.DeployResult deployRes = new Metadata.DeployResult(); 12 | deployRes.done = false; 13 | 14 | CMDDeployCallback deployCallback = new CMDDeployCallback(); 15 | CMDConnectApiDelegateMock feedApiMock = new CMDConnectApiDelegateMock(); 16 | 17 | deployCallback.feedApi = (CMDConnectApiDelegate) Test.createStub( 18 | CMDConnectApiDelegate.class, 19 | feedApiMock 20 | ); 21 | 22 | deployCallback.handleResult( 23 | deployRes, 24 | new Metadata.DeployCallbackContext() 25 | ); 26 | 27 | System.assert(!feedApiMock.postFeedElementInvoked); 28 | } 29 | 30 | @IsTest 31 | static void handleResultCreatesFeedItemWhenDeployIsSuccessful() { 32 | Metadata.DeployResult deployRes = new Metadata.DeployResult(); 33 | deployRes.id = '0Af3N00000UeFZiSAN'; 34 | deployRes.done = true; 35 | deployRes.success = true; 36 | deployRes.numberComponentErrors = 0; 37 | deployRes.numberComponentsDeployed = 10; 38 | deployRes.status = Metadata.DeployStatus.SUCCEEDED; 39 | 40 | CMDDeployCallback deployCallback = new CMDDeployCallback(); 41 | CMDConnectApiDelegateMock feedApiMock = new CMDConnectApiDelegateMock(); 42 | 43 | deployCallback.feedApi = (CMDConnectApiDelegate) Test.createStub( 44 | CMDConnectApiDelegate.class, 45 | feedApiMock 46 | ); 47 | 48 | deployCallback.handleResult( 49 | deployRes, 50 | new Metadata.DeployCallbackContext() 51 | ); 52 | 53 | System.assert(feedApiMock.postFeedElementInvoked); 54 | 55 | ConnectApi.TextSegmentInput textSegmentInput = (ConnectApi.TextSegmentInput) (feedApiMock.feedItemInput.body.messageSegments[0]); 56 | 57 | System.assert(textSegmentInput.text.contains('[' + deployRes.id + ']')); 58 | System.assert( 59 | textSegmentInput.text.contains( 60 | 'Deployment done, status: ' + Metadata.DeployStatus.SUCCEEDED.name() 61 | ) 62 | ); 63 | System.assert( 64 | textSegmentInput.text.contains( 65 | 'Components deployed: ' + deployRes.numberComponentsDeployed 66 | ) 67 | ); 68 | System.assert( 69 | textSegmentInput.text.contains( 70 | 'Errors: ' + deployRes.numberComponentErrors 71 | ) 72 | ); 73 | } 74 | 75 | @IsTest 76 | static void handleResultCreatesFeedItemWhenDeployIsUnsuccessful() { 77 | Metadata.DeployResult deployRes = new Metadata.DeployResult(); 78 | deployRes.id = '0Af3N00000UeFZiSAN'; 79 | deployRes.done = true; 80 | deployRes.success = false; 81 | deployRes.numberComponentErrors = 1; 82 | deployRes.numberComponentsDeployed = 10; 83 | deployRes.errorMessage = 'Required field is missing. MasterLabel cannot be blank'; 84 | deployRes.errorStatusCode = Metadata.StatusCode.REQUIRED_FIELD_MISSING; 85 | deployRes.status = Metadata.DeployStatus.FAILED; 86 | deployRes.details = new Metadata.DeployDetails(); 87 | deployRes.details.componentFailures = new List(); 88 | 89 | Metadata.DeployMessage errMsg1 = new Metadata.DeployMessage(); 90 | errMsg1.fullName = 'Metadata_Record_x'; 91 | errMsg1.lineNumber = 1; 92 | errMsg1.problem = 'Required field is missing'; 93 | 94 | deployRes.details.componentFailures.add(errMsg1); 95 | 96 | CMDDeployCallback deployCallback = new CMDDeployCallback(); 97 | CMDConnectApiDelegateMock feedApiMock = new CMDConnectApiDelegateMock(); 98 | 99 | deployCallback.feedApi = (CMDConnectApiDelegate) Test.createStub( 100 | CMDConnectApiDelegate.class, 101 | feedApiMock 102 | ); 103 | 104 | deployCallback.handleResult( 105 | deployRes, 106 | new Metadata.DeployCallbackContext() 107 | ); 108 | 109 | System.assert(feedApiMock.postFeedElementInvoked); 110 | 111 | ConnectApi.TextSegmentInput textSegmentInput = (ConnectApi.TextSegmentInput) (feedApiMock.feedItemInput.body.messageSegments[0]); 112 | 113 | System.assert(textSegmentInput.text.contains('[' + deployRes.id + ']')); 114 | System.assert( 115 | textSegmentInput.text.contains( 116 | 'Deployment done, status: ' + Metadata.DeployStatus.FAILED.name() 117 | ) 118 | ); 119 | System.assert(textSegmentInput.text.contains('Components deployed: 0')); 120 | System.assert( 121 | textSegmentInput.text.contains( 122 | 'Errors: ' + deployRes.numberComponentErrors 123 | ) 124 | ); 125 | System.assert(textSegmentInput.text.contains('Metadata_Record_x')); 126 | } 127 | 128 | @IsTest 129 | static void handleResultParsesOnlyFirst20ErrorMessages() { 130 | Metadata.DeployResult deployRes = new Metadata.DeployResult(); 131 | deployRes.id = '0Af3N00000UeFZiSAN'; 132 | deployRes.done = true; 133 | deployRes.success = false; 134 | deployRes.numberComponentErrors = 40; 135 | deployRes.numberComponentsDeployed = 10; 136 | deployRes.errorMessage = 'Required field is missing. MasterLabel cannot be blank'; 137 | deployRes.errorStatusCode = Metadata.StatusCode.REQUIRED_FIELD_MISSING; 138 | deployRes.status = Metadata.DeployStatus.FAILED; 139 | deployRes.details = new Metadata.DeployDetails(); 140 | deployRes.details.componentFailures = new List(); 141 | 142 | for (Integer i = 0; i < CMDDeployCallback.MAX_FAILURES + 20; i++) { 143 | Metadata.DeployMessage errMsg1 = new Metadata.DeployMessage(); 144 | errMsg1.fullName = 'Metadata_Record_x'; 145 | errMsg1.lineNumber = 1; 146 | errMsg1.problem = 'Required field is missing'; 147 | deployRes.details.componentFailures.add(errMsg1); 148 | } 149 | 150 | CMDDeployCallback deployCallback = new CMDDeployCallback(); 151 | CMDConnectApiDelegateMock feedApiMock = new CMDConnectApiDelegateMock(); 152 | 153 | deployCallback.feedApi = (CMDConnectApiDelegate) Test.createStub( 154 | CMDConnectApiDelegate.class, 155 | feedApiMock 156 | ); 157 | 158 | deployCallback.handleResult( 159 | deployRes, 160 | new Metadata.DeployCallbackContext() 161 | ); 162 | 163 | System.assert(feedApiMock.postFeedElementInvoked); 164 | 165 | ConnectApi.TextSegmentInput textSegmentInput = (ConnectApi.TextSegmentInput) (feedApiMock.feedItemInput.body.messageSegments[0]); 166 | 167 | System.assert(textSegmentInput.text.contains('...20 more, go to "Setup -> Deployment Status" for additional details')); 168 | } 169 | 170 | public class CMDConnectApiDelegateMock implements System.StubProvider { 171 | public Boolean postFeedElementInvoked; 172 | public ConnectApi.FeedItemInput feedItemInput; 173 | 174 | public CMDConnectApiDelegateMock() { 175 | this.postFeedElementInvoked = false; 176 | } 177 | 178 | public Object handleMethodCall( 179 | Object stubbedObject, 180 | String stubbedMethodName, 181 | Type returnType, 182 | List listOfParamTypes, 183 | List listOfParamNames, 184 | List listOfArgs 185 | ) { 186 | if (stubbedMethodName == 'postFeedElement') { 187 | this.postFeedElementInvoked = true; 188 | this.feedItemInput = (ConnectApi.FeedItemInput) listOfArgs[1]; 189 | } 190 | 191 | return null; 192 | } 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /cmd-loader/main/default/lwc/cmdLoader/cmdLoader.html: -------------------------------------------------------------------------------- 1 | 7 | 202 | -------------------------------------------------------------------------------- /cmd-loader/main/default/classes/CMDLoaderController.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Marco Zeuli 3 | * Licensed under MIT license. 4 | * For full license text, see LICENSE file in the repo root or https://opensource.org/licenses/MIT 5 | * If you would like to contribute https://github.com/maaaaarco/spaghetti-cmd-loader 6 | */ 7 | 8 | /** 9 | * Controller class for cmdLoader LWC 10 | */ 11 | public with sharing class CMDLoaderController { 12 | @TestVisible 13 | static CMDConnectApiDelegate feedApi = new CMDConnectApiDelegate(); 14 | 15 | @TestVisible 16 | static CMDMetadataOperationsDelegate deployApi = new CMDMetadataOperationsDelegate(); 17 | 18 | 19 | public class RecordWrapper { 20 | @AuraEnabled 21 | public List fields { get; set; } 22 | 23 | public RecordWrapper() { 24 | this.fields = new List(); 25 | } 26 | } 27 | 28 | public class FieldWrapper { 29 | @AuraEnabled 30 | public String fieldName { get; set; } 31 | @AuraEnabled 32 | public String fieldValue { get; set; } 33 | } 34 | 35 | public class DeploymentStatusWrapper { 36 | @AuraEnabled 37 | public Boolean done { get; set; } 38 | @AuraEnabled 39 | public Boolean success { get; set; } 40 | @AuraEnabled 41 | public String result { get; set; } 42 | @AuraEnabled 43 | public String id { get; set; } 44 | @AuraEnabled 45 | public Integer count { get; set; } 46 | 47 | public DeploymentStatusWrapper(String id) { 48 | done = false; 49 | success = false; 50 | this.id = id; 51 | } 52 | } 53 | 54 | /** 55 | * Returns a list of available Custom Metadata Types 56 | * @return a list containing Custom Metadata Type API names or an empty one 57 | */ 58 | @AuraEnabled 59 | public static List retrieveCustomMetadataTypes() { 60 | List res = new List(); 61 | for (EntityDefinition ed : [ 62 | SELECT QualifiedApiName 63 | FROM EntityDefinition 64 | WHERE IsCustomizable = true AND KeyPrefix LIKE 'm%' 65 | ORDER BY QualifiedApiName ASC 66 | ]) { 67 | if (ed.QualifiedApiName.endsWithIgnoreCase(CMDConstants.MDT_SUFFIX)) { 68 | res.add(ed.QualifiedApiName); 69 | } 70 | } 71 | return res; 72 | } 73 | 74 | /** 75 | * Checks deployment's result by querying the chatter feed of current user. 76 | * NOTE: 77 | * I consider this an hack but I was not able to figure out a quick and easy way to 78 | * invoke REST Metadata API from Apex. I don't want to use the SOAP Metadata API because 79 | * I do not want to generate and maintain the wsdl classes. 80 | * @param deployId Deployment id 81 | * @return An instance of DeploymentStatusWrapper 82 | */ 83 | @AuraEnabled 84 | public static DeploymentStatusWrapper checkDeployment(String deployId) { 85 | DeploymentStatusWrapper res = new DeploymentStatusWrapper(deployId); 86 | 87 | try { 88 | for (FeedItem feed : feedApi.getTodayFeedItems(UserInfo.getUserId())) { 89 | // looks for deploymentId value in the chatter post 90 | if (feed.Body.containsIgnoreCase(deployId)) { 91 | res.done = true; 92 | // search the word Succeeded in the post 93 | res.success = feed.Body.split('\n')[0] 94 | .contains(Metadata.DeployStatus.SUCCEEDED.name()); 95 | res.result = feed.Body; 96 | } 97 | } 98 | } catch (Exception unexpected) { 99 | res.done = true; 100 | res.success = false; 101 | res.result = 102 | 'Unexpected exception occurred while checking deployment status, message is: ' + 103 | unexpected.getMessage(); 104 | } 105 | 106 | return res; 107 | } 108 | 109 | /** 110 | * Insert or update existing Custom Metadata records 111 | * @param cmdType Custom Metadata Type api name 112 | * @param records Records to insert/update 113 | * @return The Metadata Api deployment id 114 | * @throws AuraHandledException if an error occurs 115 | */ 116 | @AuraEnabled 117 | public static Id upsertRecords(String cmdType, List records) { 118 | Id deployJobId = null; 119 | 120 | try { 121 | // in case cmdType is not a valid object this will throw an exception 122 | Schema.DescribeSObjectResult[] objMetadata = Schema.describeSObjects( 123 | new List{ cmdType } 124 | ); 125 | 126 | Map fieldMap = objMetadata[0] 127 | .fields.getMap(); 128 | 129 | // component's name without __mdt suffix, used to set fullName attribute on records 130 | final String cmdTypeBaseName = cmdType.replace( 131 | CMDConstants.MDT_SUFFIX, 132 | '' 133 | ); 134 | 135 | // deploy container 136 | Metadata.DeployContainer container = new Metadata.DeployContainer(); 137 | 138 | for (RecordWrapper recordWrapper : records) { 139 | Metadata.CustomMetadata customMetadataRecord = new Metadata.CustomMetadata(); 140 | 141 | for (FieldWrapper fieldWrapper : recordWrapper.fields) { 142 | if ( 143 | fieldWrapper.fieldName == CMDConstants.DEVELOPER_NAME_FIELD_NAME 144 | ) { 145 | customMetadataRecord.fullName = 146 | cmdTypeBaseName + 147 | '.' + 148 | fieldWrapper.fieldValue; 149 | } else if ( 150 | CMDConstants.MASTER_LABEL_FIELD_NAMES.contains( 151 | fieldWrapper.fieldName 152 | ) 153 | ) { 154 | customMetadataRecord.label = fieldWrapper.fieldValue; 155 | } else { 156 | Schema.SObjectField objField = fieldMap.get(fieldWrapper.fieldName); 157 | 158 | if (objField == null) { 159 | throw new AuraHandledException( 160 | String.format( 161 | 'Field {0} does not exist on {1}. CSV columns must match field API name.', 162 | new List{ fieldWrapper.fieldName, cmdType } 163 | ) 164 | ); 165 | } 166 | 167 | Schema.DisplayType fieldType = getFieldDisplayType( 168 | fieldWrapper.fieldName, 169 | objField 170 | ); 171 | 172 | Metadata.CustomMetadataValue customField = new Metadata.CustomMetadataValue(); 173 | customField.field = fieldWrapper.fieldName; 174 | 175 | if (String.isNotBlank(fieldWrapper.fieldValue)) { 176 | // cast generic string value to proper primitive type 177 | switch on fieldType { 178 | when BOOLEAN { 179 | customField.value = (Object) Boolean.valueOf( 180 | fieldWrapper.fieldValue 181 | ); 182 | } 183 | when DATE { 184 | customField.value = (Object) Date.valueOf( 185 | fieldWrapper.fieldValue 186 | ); 187 | } 188 | when DATETIME { 189 | customField.value = (Object) Datetime.valueOf( 190 | fieldWrapper.fieldValue 191 | ); 192 | } 193 | when DOUBLE, PERCENT { 194 | customField.value = (Object) Double.valueOf( 195 | fieldWrapper.fieldValue 196 | ); 197 | } 198 | when INTEGER { 199 | customField.value = (Object) Integer.valueOf( 200 | fieldWrapper.fieldValue 201 | ); 202 | } 203 | when LONG { 204 | customField.value = (Object) Long.valueOf( 205 | fieldWrapper.fieldValue 206 | ); 207 | } 208 | when EMAIL, PICKLIST, REFERENCE, STRING, TEXTAREA, URL, PHONE { 209 | // string 210 | customField.value = fieldWrapper.fieldValue; 211 | } 212 | when else { 213 | // tries with string 214 | customField.value = fieldWrapper.fieldValue; 215 | } 216 | } 217 | } else { 218 | customField.value = null; 219 | } 220 | 221 | customMetadataRecord.values.add(customField); 222 | } 223 | } 224 | 225 | container.addMetadata(customMetadataRecord); 226 | } 227 | 228 | deployJobId = deployApi.enqueueDeployment( 229 | container, 230 | new CMDDeployCallback() 231 | ); 232 | 233 | if (String.isBlank(deployJobId)) { 234 | // it is not 100% clear why this happen, usually with large CSV file 235 | throw new AuraHandledException( 236 | 'Failed to schedule deploy. If your CSV file is very big (more than 500 rows) try to split it in smaller chunks and try again' 237 | ); 238 | } 239 | } catch (AuraHandledException reThrow) { 240 | throw reThrow; 241 | } catch (Exception unexpected) { 242 | throw new AuraHandledException( 243 | String.format( 244 | 'Unexpected exception occurred, message is: {0}\nStackTrace: {1}', 245 | new List{ 246 | unexpected.getMessage(), 247 | unexpected.getStackTraceString() 248 | } 249 | ) 250 | ); 251 | } 252 | 253 | return deployJobId; 254 | } 255 | 256 | private static Map FIELD_TO_DISPLAY_TYPE_CACHE = new Map(); 257 | private static Schema.DisplayType getFieldDisplayType( 258 | String fieldName, 259 | Schema.SObjectField objField 260 | ) { 261 | Schema.DisplayType res = FIELD_TO_DISPLAY_TYPE_CACHE.get(fieldName); 262 | if (res == null) { 263 | res = objField.getDescribe().getType(); 264 | FIELD_TO_DISPLAY_TYPE_CACHE.put(fieldName, res); 265 | } 266 | return res; 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /cmd-loader/main/default/lwc/cmdLoader/cmdLoader.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Marco Zeuli 3 | * Licensed under MIT license. 4 | * For full license text, see LICENSE file in the repo root or https://opensource.org/licenses/MIT 5 | * If you would like to contribute https://github.com/maaaaarco/spaghetti-cmd-loader 6 | */ 7 | 8 | import { LightningElement, track } from "lwc"; 9 | import retrieveCustomMetadataTypes from "@salesforce/apex/CMDLoaderController.retrieveCustomMetadataTypes"; 10 | import { ShowToastEvent } from "lightning/platformShowToastEvent"; 11 | import PapaParse from "@salesforce/resourceUrl/PapaParse"; 12 | import { loadScript } from "lightning/platformResourceLoader"; 13 | import upsertRecords from "@salesforce/apex/CMDLoaderController.upsertRecords"; 14 | import checkDeployment from "@salesforce/apex/CMDLoaderController.checkDeployment"; 15 | 16 | const MAX_PREVIEW_ROWS = 250; 17 | const DEPLOYMENT_FAILED_FOR_UNKNOWN_REASON = { 18 | body: { 19 | message: 20 | "Failed to schedule deploy. If your CSV file is very big (more than 500 rows) try to split it in smaller chunks and try again" 21 | } 22 | }; 23 | 24 | export default class CmdLoader extends LightningElement { 25 | @track cmdTypes = []; 26 | @track cmdRecords = []; 27 | 28 | @track columns = []; 29 | 30 | @track selectedType; 31 | 32 | @track deployResults = []; 33 | 34 | @track csvHasDeveloperName = false; 35 | @track csvHasMasterLabel = false; 36 | 37 | @track showPreviewAnyway = false; 38 | 39 | @track deployCounter = 0; 40 | 41 | @track validationInProgress = false; 42 | @track warningMessages = []; 43 | 44 | papaParseLoaded = false; 45 | deploymentId; 46 | checkDeployIntervalId; 47 | 48 | masterLabelColumn; 49 | developerNameColumn; 50 | 51 | get hasAllRequiredColumns() { 52 | return ( 53 | !this.fileParsed || (this.csvHasDeveloperName && this.csvHasMasterLabel) 54 | ); 55 | } 56 | 57 | get showPreview() { 58 | return ( 59 | this.fileParsed && (!this.tooManyRowsForPreview || this.showPreviewAnyway) 60 | ); 61 | } 62 | 63 | get tooManyRowsForPreview() { 64 | return this.cmdRecords.length > MAX_PREVIEW_ROWS; 65 | } 66 | 67 | get disableLoad() { 68 | return ( 69 | !this.fileParsed || 70 | !this.selectedType || 71 | !this.hasAllRequiredColumns || 72 | this.deploymentId 73 | ); 74 | } 75 | 76 | get numberOfDeploys() { 77 | return Math.ceil(this.cmdRecords.length / MAX_PREVIEW_ROWS); 78 | } 79 | 80 | get hasWarningMessages() { 81 | return ( 82 | this.fileParsed && 83 | !this.validationInProgress && 84 | this.warningMessages.length 85 | ); 86 | } 87 | 88 | get fileParsed() { 89 | return !!this.cmdRecords.length; 90 | } 91 | 92 | renderedCallback() { 93 | if (this.papaParseLoaded) { 94 | return; 95 | } 96 | 97 | loadScript(this, PapaParse + "/papaparse.min.js") 98 | .then(() => { 99 | this.papaParseLoaded = true; 100 | }) 101 | .catch(err => { 102 | this._dispatchError(err); 103 | }); 104 | } 105 | 106 | connectedCallback() { 107 | retrieveCustomMetadataTypes() 108 | .then(data => { 109 | this.cmdTypes = data; 110 | }) 111 | .catch(err => { 112 | this._dispatchError(err); 113 | }); 114 | } 115 | 116 | handleCmdTypeSelection(event) { 117 | this.selectedType = event.target.value; 118 | } 119 | 120 | handleFileChange(event) { 121 | this._resetState(); 122 | if (event.target.files && event.target.files.length) { 123 | this._parseCsvAndDisplayPreview(event.target.files[0]); 124 | } 125 | } 126 | 127 | handleCellChange(event) { 128 | const cell = event.detail.draftValues[0]; 129 | // index in the cmdRecords array 130 | const idx = /row-\d+/.test(cell.key) ? cell.key.split("-")[1] : undefined; 131 | if (idx) { 132 | for (let fieldName in cell) { 133 | if (fieldName !== "key") { 134 | this.cmdRecords[idx][fieldName] = cell[fieldName]; 135 | } 136 | } 137 | } 138 | 139 | this._startValidateRecords(); 140 | } 141 | 142 | enableShowPreviewAnyway(event) { 143 | event.preventDefault(); 144 | this.showPreviewAnyway = true; 145 | } 146 | 147 | loadRecords() { 148 | const recordWrappers = []; 149 | 150 | const startIdx = this.deployCounter * MAX_PREVIEW_ROWS; 151 | 152 | if (startIdx >= this.cmdRecords.length) { 153 | return; // deployment complete 154 | } 155 | 156 | const cmdRecordsToDeploy = this.cmdRecords.slice( 157 | startIdx, 158 | startIdx + MAX_PREVIEW_ROWS 159 | ); 160 | 161 | cmdRecordsToDeploy.forEach(cmdRecord => { 162 | const recordWrapper = { 163 | fields: [] 164 | }; 165 | 166 | this.columns 167 | .map(col => col.fieldName) 168 | .forEach(field => { 169 | recordWrapper.fields.push({ 170 | fieldName: field, 171 | fieldValue: cmdRecord[field] 172 | }); 173 | }); 174 | 175 | recordWrappers.push(recordWrapper); 176 | }); 177 | 178 | upsertRecords({ 179 | cmdType: this.selectedType, 180 | records: recordWrappers 181 | }) 182 | .then(data => { 183 | if (data) { 184 | this.deploymentId = data; 185 | this.deployCounter++; 186 | this._startCheckDeployPolling(); 187 | } else { 188 | // it's not clear why this happen since I'd expect the controller to catch it and throw an exception 189 | // but it does sometime especially with large CSV file 190 | this._dispatchError(DEPLOYMENT_FAILED_FOR_UNKNOWN_REASON); 191 | this._clearIntervals(); 192 | } 193 | }) 194 | .catch(err => { 195 | this._dispatchError(err); 196 | }); 197 | } 198 | 199 | _parseCsvAndDisplayPreview(file) { 200 | const fileReader = new FileReader(); 201 | 202 | fileReader.addEventListener("load", event => { 203 | const parseResult = Papa.parse(event.target.result, { 204 | header: true, 205 | skipEmptyLines: true 206 | }); 207 | 208 | // creates headers for data-table 209 | if (parseResult.data.length) { 210 | const headers = Object.keys(parseResult.data[0]); 211 | 212 | headers.forEach(header => { 213 | this.columns.push({ 214 | label: header, 215 | fieldName: header, 216 | type: "text", 217 | editable: true 218 | }); 219 | 220 | if (/^DeveloperName$/i.test(header)) { 221 | this.csvHasDeveloperName = true; 222 | this.developerNameColumn = header; 223 | } 224 | 225 | if (/^(Master)?Label$/i.test(header)) { 226 | this.csvHasMasterLabel = true; 227 | this.masterLabelColumn = header; 228 | } 229 | }); 230 | } 231 | 232 | this.cmdRecords = parseResult.data; 233 | // creates a key field 234 | this.cmdRecords.forEach((record, idx) => { 235 | record.key = `row-${idx}`; 236 | }); 237 | this._startValidateRecords(); 238 | }); 239 | 240 | fileReader.readAsText(file); 241 | } 242 | 243 | _startValidateRecords() { 244 | if (this.hasAllRequiredColumns) { 245 | this.validationInProgress = true; 246 | // eslint-disable-next-line @lwc/lwc/no-async-operation 247 | setTimeout(() => { 248 | this._validateRecords(); 249 | }, 2000); 250 | } 251 | } 252 | 253 | _validateRecords() { 254 | const warnings = []; 255 | 256 | // stores a count for each developer name 257 | const devNameCount = {}; 258 | 259 | this.cmdRecords.forEach((record, idx) => { 260 | const devName = record[this.developerNameColumn]; 261 | const label = record[this.masterLabelColumn]; 262 | 263 | if (!devName) { 264 | warnings.push(`Row ${idx + 1} - Developer Name cannot be blank`); 265 | } else { 266 | if (devNameCount[devName]) { 267 | warnings.push( 268 | `Row ${idx + 269 | 1} - Developer Name ${devName} occurs more than one time` 270 | ); 271 | } 272 | devNameCount[devName] = 1; 273 | } 274 | 275 | if (!label) { 276 | warnings.push(`Row ${idx + 1} - Label cannot be blank`); 277 | } 278 | }); 279 | 280 | this.warningMessages = warnings; 281 | this.validationInProgress = false; 282 | } 283 | 284 | _resetState() { 285 | this.deployResults = []; 286 | this.cmdRecords = []; 287 | this.columns = []; 288 | this.deploymentId = undefined; 289 | this.csvHasDeveloperName = false; 290 | this.csvHasMasterLabel = false; 291 | this.showPreviewAnyway = false; 292 | this.deployCounter = 0; 293 | this.warningMessages = []; 294 | this.validationInProgress = false; 295 | } 296 | 297 | _startCheckDeployPolling() { 298 | if (!this.checkDeployIntervalId) { 299 | // eslint-disable-next-line @lwc/lwc/no-async-operation 300 | this.checkDeployIntervalId = setInterval(() => { 301 | checkDeployment({ 302 | deployId: this.deploymentId 303 | }) 304 | .then(response => { 305 | response.count = this.deployCounter; 306 | if (this.deployResults.length != this.deployCounter) { 307 | this.deployResults.push(response); 308 | } else { 309 | this.deployResults[this.deployResults.length - 1] = response; 310 | } 311 | if (response.done) { 312 | // invokes loadRecords in case there are still records to process 313 | this.loadRecords(); 314 | this._clearIntervals(); 315 | } 316 | }) 317 | .catch(err => { 318 | this._dispatchError(err); 319 | this._clearIntervals(); 320 | }); 321 | }, 2000); 322 | } 323 | } 324 | 325 | _clearIntervals() { 326 | if (this.checkDeployIntervalId) { 327 | clearInterval(this.checkDeployIntervalId); 328 | this.checkDeployIntervalId = null; 329 | } 330 | } 331 | 332 | _dispatchError(err) { 333 | const toastEvent = new ShowToastEvent({ 334 | message: err.body.message, 335 | variant: "error", 336 | mode: "sticky" 337 | }); 338 | this.dispatchEvent(toastEvent); 339 | } 340 | } 341 | -------------------------------------------------------------------------------- /cmd-loader/main/default/classes/CMDLoaderControllerTest.cls: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Marco Zeuli 3 | * Licensed under MIT license. 4 | * For full license text, see LICENSE file in the repo root or https://opensource.org/licenses/MIT 5 | * If you would like to contribute https://github.com/maaaaarco/spaghetti-cmd-loader 6 | */ 7 | @IsTest 8 | private class CMDLoaderControllerTest { 9 | static final Id DEPLOYMENT_ID = '0Af3N00000UeFZiSAN'; 10 | 11 | @IsTest 12 | static void retrieveCustomMetadataTypesReturnsCorrectly() { 13 | List res = CMDLoaderController.retrieveCustomMetadataTypes(); 14 | System.assertNotEquals(null, res); 15 | } 16 | 17 | @IsTest 18 | static void checkDeploymentReturnsCorrectlyWhenDeploymentIsSuccessful() { 19 | CMDConnectApiDelegateMock feedApiMock = new CMDConnectApiDelegateMock(); 20 | CMDLoaderController.feedApi = (CMDConnectApiDelegate) Test.createStub( 21 | CMDConnectApiDelegate.class, 22 | feedApiMock 23 | ); 24 | CMDLoaderController.DeploymentStatusWrapper res = CMDLoaderController.checkDeployment( 25 | DEPLOYMENT_ID 26 | ); 27 | System.assert(feedApiMock.getTodayFeedItemsInvoked); 28 | System.assertNotEquals(null, res); 29 | System.assertEquals(DEPLOYMENT_ID, res.id); 30 | System.assert(res.done); 31 | System.assert(res.success); 32 | System.assert(String.isNotBlank(res.result)); 33 | } 34 | 35 | @IsTest 36 | static void checkDeploymentReturnsCorrectlyWhenDeploymentIsUnsuccessful() { 37 | CMDConnectApiDelegateMock feedApiMock = new CMDConnectApiDelegateMock( 38 | Metadata.deployStatus.FAILED 39 | ); 40 | CMDLoaderController.feedApi = (CMDConnectApiDelegate) Test.createStub( 41 | CMDConnectApiDelegate.class, 42 | feedApiMock 43 | ); 44 | CMDLoaderController.DeploymentStatusWrapper res = CMDLoaderController.checkDeployment( 45 | DEPLOYMENT_ID 46 | ); 47 | System.assert(feedApiMock.getTodayFeedItemsInvoked); 48 | System.assertNotEquals(null, res); 49 | System.assertEquals(DEPLOYMENT_ID, res.id); 50 | System.assert(res.done); 51 | System.assert(!res.success); 52 | System.assert(String.isNotBlank(res.result)); 53 | } 54 | 55 | @IsTest 56 | static void checkDeploymentReturnsCorrectlyIfUnexpectedExceptionOccurred() { 57 | CMDConnectApiDelegateMock feedApiMock = new CMDConnectApiDelegateMock(true); 58 | CMDLoaderController.feedApi = (CMDConnectApiDelegate) Test.createStub( 59 | CMDConnectApiDelegate.class, 60 | feedApiMock 61 | ); 62 | CMDLoaderController.DeploymentStatusWrapper res = CMDLoaderController.checkDeployment( 63 | DEPLOYMENT_ID 64 | ); 65 | System.assertNotEquals(null, res); 66 | System.assertEquals(DEPLOYMENT_ID, res.id); 67 | System.assert(res.done); 68 | System.assert(!res.success); 69 | System.assert(String.isNotBlank(res.result)); 70 | } 71 | 72 | @IsTest 73 | static void upsertRecordsReturnsDeployIdCorrectly() { 74 | CMDMetadataOperationsDelegateMock deployApiMock = new CMDMetadataOperationsDelegateMock(); 75 | CMDLoaderController.deployApi = (CMDMetadataOperationsDelegate) Test.createStub( 76 | CMDMetadataOperationsDelegate.class, 77 | deployApiMock 78 | ); 79 | // seems silly but it works :P 80 | String cmdType = 'Opportunity'; 81 | CMDLoaderController.RecordWrapper record = new CMDLoaderController.RecordWrapper(); 82 | 83 | CMDLoaderController.FieldWrapper devName = new CMDLoaderController.FieldWrapper(); 84 | devName.fieldName = 'DeveloperName'; 85 | devName.fieldValue = 'abc'; 86 | 87 | CMDLoaderController.FieldWrapper label = new CMDLoaderController.FieldWrapper(); 88 | label.fieldName = 'MasterLabel'; 89 | label.fieldValue = 'the label'; 90 | 91 | CMDLoaderController.FieldWrapper boolField = new CMDLoaderController.FieldWrapper(); 92 | boolField.fieldName = 'IsClosed'; 93 | boolField.fieldValue = 'false'; 94 | 95 | CMDLoaderController.FieldWrapper dateField = new CMDLoaderController.FieldWrapper(); 96 | dateField.fieldName = 'CloseDate'; 97 | dateField.fieldValue = '2020-01-01'; 98 | 99 | CMDLoaderController.FieldWrapper dateTimeField = new CMDLoaderController.FieldWrapper(); 100 | dateTimeField.fieldName = 'CreatedDate'; 101 | dateTimeField.fieldValue = '2020-01-01 00:00:01'; 102 | 103 | CMDLoaderController.FieldWrapper doubleField = new CMDLoaderController.FieldWrapper(); 104 | doubleField.fieldName = 'Amount'; 105 | doubleField.fieldValue = '1000.00'; 106 | 107 | CMDLoaderController.FieldWrapper integerField = new CMDLoaderController.FieldWrapper(); 108 | integerField.fieldName = 'TotalOpportunityQuantity'; 109 | integerField.fieldValue = '10'; 110 | 111 | CMDLoaderController.FieldWrapper textField = new CMDLoaderController.FieldWrapper(); 112 | textField.fieldName = 'Description'; 113 | textField.fieldValue = 'Oppty description'; 114 | 115 | CMDLoaderController.FieldWrapper blankField = new CMDLoaderController.FieldWrapper(); 116 | blankField.fieldName = 'AccountId'; 117 | 118 | record.fields.add(devName); 119 | record.fields.add(label); 120 | record.fields.add(boolField); 121 | record.fields.add(dateField); 122 | record.fields.add(dateTimeField); 123 | record.fields.add(doubleField); 124 | record.fields.add(integerField); 125 | record.fields.add(textField); 126 | record.fields.add(blankField); 127 | 128 | Id res = CMDLoaderController.upsertRecords( 129 | cmdType, 130 | new List{ record } 131 | ); 132 | 133 | System.assert(deployApiMock.enqueueDeploymentInvoked); 134 | System.assertEquals(DEPLOYMENT_ID, res); 135 | } 136 | 137 | @IsTest 138 | static void upsertRecordsThrowsAuraExceptionIfFieldDoesNotExist() { 139 | CMDMetadataOperationsDelegateMock deployApiMock = new CMDMetadataOperationsDelegateMock(); 140 | CMDLoaderController.deployApi = (CMDMetadataOperationsDelegate) Test.createStub( 141 | CMDMetadataOperationsDelegate.class, 142 | deployApiMock 143 | ); 144 | // seems silly but it works :P 145 | String cmdType = 'Opportunity'; 146 | CMDLoaderController.RecordWrapper record = new CMDLoaderController.RecordWrapper(); 147 | 148 | CMDLoaderController.FieldWrapper devName = new CMDLoaderController.FieldWrapper(); 149 | devName.fieldName = 'DeveloperName'; 150 | devName.fieldValue = 'abc'; 151 | 152 | CMDLoaderController.FieldWrapper label = new CMDLoaderController.FieldWrapper(); 153 | label.fieldName = 'MasterLabel'; 154 | label.fieldValue = 'the label'; 155 | 156 | CMDLoaderController.FieldWrapper boolField = new CMDLoaderController.FieldWrapper(); 157 | boolField.fieldName = 'spaghettiCMD__PastaAlPomodoro__c'; 158 | boolField.fieldValue = 'false'; 159 | 160 | record.fields.add(devName); 161 | record.fields.add(label); 162 | record.fields.add(boolField); 163 | 164 | Boolean thrown = false; 165 | 166 | try { 167 | CMDLoaderController.upsertRecords( 168 | cmdType, 169 | new List{ record } 170 | ); 171 | } catch (AuraHandledException expected) { 172 | thrown = true; 173 | } 174 | 175 | System.assert(thrown); 176 | System.assert(!deployApiMock.enqueueDeploymentInvoked); 177 | } 178 | 179 | @IsTest 180 | static void upsertRecordsThrowsAuraExceptionIfDeployIdIsNull() { 181 | CMDMetadataOperationsDelegateMock deployApiMock = new CMDMetadataOperationsDelegateMock(); 182 | deployApiMock.deployId = null; 183 | CMDLoaderController.deployApi = (CMDMetadataOperationsDelegate) Test.createStub( 184 | CMDMetadataOperationsDelegate.class, 185 | deployApiMock 186 | ); 187 | // seems silly but it works :P 188 | String cmdType = 'Opportunity'; 189 | CMDLoaderController.RecordWrapper record = new CMDLoaderController.RecordWrapper(); 190 | 191 | CMDLoaderController.FieldWrapper devName = new CMDLoaderController.FieldWrapper(); 192 | devName.fieldName = 'DeveloperName'; 193 | devName.fieldValue = 'abc'; 194 | 195 | CMDLoaderController.FieldWrapper label = new CMDLoaderController.FieldWrapper(); 196 | label.fieldName = 'MasterLabel'; 197 | label.fieldValue = 'the label'; 198 | 199 | record.fields.add(devName); 200 | record.fields.add(label); 201 | 202 | Boolean thrown = false; 203 | 204 | try { 205 | CMDLoaderController.upsertRecords( 206 | cmdType, 207 | new List{ record } 208 | ); 209 | } catch (AuraHandledException expected) { 210 | thrown = true; 211 | } 212 | 213 | System.assert(thrown); 214 | System.assert(deployApiMock.enqueueDeploymentInvoked); 215 | } 216 | 217 | @IsTest 218 | static void upsertRecordsThrowsAuraExceptionIfUnexpectedErrorOccurrs() { 219 | CMDMetadataOperationsDelegateMock deployApiMock = new CMDMetadataOperationsDelegateMock(true); 220 | CMDLoaderController.deployApi = (CMDMetadataOperationsDelegate) Test.createStub( 221 | CMDMetadataOperationsDelegate.class, 222 | deployApiMock 223 | ); 224 | // seems silly but it works :P 225 | String cmdType = 'Opportunity'; 226 | CMDLoaderController.RecordWrapper record = new CMDLoaderController.RecordWrapper(); 227 | 228 | CMDLoaderController.FieldWrapper devName = new CMDLoaderController.FieldWrapper(); 229 | devName.fieldName = 'DeveloperName'; 230 | devName.fieldValue = 'abc'; 231 | 232 | CMDLoaderController.FieldWrapper label = new CMDLoaderController.FieldWrapper(); 233 | label.fieldName = 'MasterLabel'; 234 | label.fieldValue = 'the label'; 235 | 236 | record.fields.add(devName); 237 | record.fields.add(label); 238 | 239 | Boolean thrown = false; 240 | 241 | try { 242 | CMDLoaderController.upsertRecords( 243 | cmdType, 244 | new List{ record } 245 | ); 246 | } catch (AuraHandledException expected) { 247 | thrown = true; 248 | } 249 | 250 | System.assert(thrown); 251 | } 252 | 253 | public class CMDConnectApiDelegateMock implements System.StubProvider { 254 | public Boolean getTodayFeedItemsInvoked; 255 | public Boolean throwException; 256 | public Metadata.DeployStatus deployStatus; 257 | 258 | public CMDConnectApiDelegateMock() { 259 | this.getTodayFeedItemsInvoked = false; 260 | this.throwException = false; 261 | this.deployStatus = Metadata.DeployStatus.SUCCEEDED; 262 | } 263 | 264 | public CMDConnectApiDelegateMock(Boolean throwException) { 265 | this(); 266 | this.throwException = throwException; 267 | } 268 | 269 | public CMDConnectApiDelegateMock(Metadata.DeployStatus deployStatus) { 270 | this(); 271 | this.deployStatus = deployStatus; 272 | } 273 | 274 | public Object handleMethodCall( 275 | Object stubbedObject, 276 | String stubbedMethodName, 277 | Type returnType, 278 | List listOfParamTypes, 279 | List listOfParamNames, 280 | List listOfArgs 281 | ) { 282 | if (throwException) { 283 | Integer i = 1 / 0; 284 | } 285 | 286 | if (stubbedMethodName == 'getTodayFeedItems') { 287 | this.getTodayFeedItemsInvoked = true; 288 | return new List{ 289 | new FeedItem( 290 | Body = '[' + 291 | DEPLOYMENT_ID + 292 | '] Deployment done, status is: ' + 293 | this.deployStatus.name() 294 | ) 295 | }; 296 | } 297 | return null; 298 | } 299 | } 300 | 301 | public class CMDMetadataOperationsDelegateMock implements System.StubProvider { 302 | public Boolean enqueueDeploymentInvoked; 303 | public Boolean throwException; 304 | public Id deployId; 305 | 306 | public CMDMetadataOperationsDelegateMock() { 307 | this.deployId = DEPLOYMENT_ID; 308 | this.throwException = false; 309 | this.enqueueDeploymentInvoked = false; 310 | } 311 | 312 | public CMDMetadataOperationsDelegateMock(Boolean throwException) { 313 | this(); 314 | this.throwException = true; 315 | } 316 | 317 | public Object handleMethodCall( 318 | Object stubbedObject, 319 | String stubbedMethodName, 320 | Type returnType, 321 | List listOfParamTypes, 322 | List listOfParamNames, 323 | List listOfArgs 324 | ) { 325 | if (throwException) { 326 | Integer i = 1/0; 327 | } 328 | 329 | if (stubbedMethodName == 'enqueueDeployment') { 330 | this.enqueueDeploymentInvoked = true; 331 | return this.deployId; 332 | } 333 | return null; 334 | } 335 | } 336 | } 337 | -------------------------------------------------------------------------------- /cmd-loader/main/default/staticresources/PapaParse.js: -------------------------------------------------------------------------------- 1 | /* @license 2 | Papa Parse 3 | v5.2.0 4 | https://github.com/mholt/PapaParse 5 | License: MIT 6 | */ 7 | !function(e,t){"function"==typeof define&&define.amd?define([],t):"object"==typeof module&&"undefined"!=typeof exports?module.exports=t():e.Papa=t()}(this,function s(){"use strict";var f="undefined"!=typeof self?self:"undefined"!=typeof window?window:void 0!==f?f:{};var n=!f.document&&!!f.postMessage,o=n&&/blob:/i.test((f.location||{}).protocol),a={},h=0,b={parse:function(e,t){var i=(t=t||{}).dynamicTyping||!1;U(i)&&(t.dynamicTypingFunction=i,i={});if(t.dynamicTyping=i,t.transform=!!U(t.transform)&&t.transform,t.worker&&b.WORKERS_SUPPORTED){var r=function(){if(!b.WORKERS_SUPPORTED)return!1;var e=(i=f.URL||f.webkitURL||null,r=s.toString(),b.BLOB_URL||(b.BLOB_URL=i.createObjectURL(new Blob(["(",r,")();"],{type:"text/javascript"})))),t=new f.Worker(e);var i,r;return t.onmessage=_,t.id=h++,a[t.id]=t}();return r.userStep=t.step,r.userChunk=t.chunk,r.userComplete=t.complete,r.userError=t.error,t.step=U(t.step),t.chunk=U(t.chunk),t.complete=U(t.complete),t.error=U(t.error),delete t.worker,void r.postMessage({input:e,config:t,workerId:r.id})}var n=null;b.NODE_STREAM_INPUT,"string"==typeof e?n=t.download?new l(t):new p(t):!0===e.readable&&U(e.read)&&U(e.on)?n=new g(t):(f.File&&e instanceof File||e instanceof Object)&&(n=new c(t));return n.stream(e)},unparse:function(e,t){var n=!1,_=!0,m=",",v="\r\n",s='"',a=s+s,i=!1,r=null;!function(){if("object"!=typeof t)return;"string"!=typeof t.delimiter||b.BAD_DELIMITERS.filter(function(e){return-1!==t.delimiter.indexOf(e)}).length||(m=t.delimiter);("boolean"==typeof t.quotes||"function"==typeof t.quotes||Array.isArray(t.quotes))&&(n=t.quotes);"boolean"!=typeof t.skipEmptyLines&&"string"!=typeof t.skipEmptyLines||(i=t.skipEmptyLines);"string"==typeof t.newline&&(v=t.newline);"string"==typeof t.quoteChar&&(s=t.quoteChar);"boolean"==typeof t.header&&(_=t.header);if(Array.isArray(t.columns)){if(0===t.columns.length)throw new Error("Option columns is empty");r=t.columns}void 0!==t.escapeChar&&(a=t.escapeChar+s)}();var o=new RegExp(q(s),"g");"string"==typeof e&&(e=JSON.parse(e));if(Array.isArray(e)){if(!e.length||Array.isArray(e[0]))return u(null,e,i);if("object"==typeof e[0])return u(r||h(e[0]),e,i)}else if("object"==typeof e)return"string"==typeof e.data&&(e.data=JSON.parse(e.data)),Array.isArray(e.data)&&(e.fields||(e.fields=e.meta&&e.meta.fields),e.fields||(e.fields=Array.isArray(e.data[0])?e.fields:h(e.data[0])),Array.isArray(e.data[0])||"object"==typeof e.data[0]||(e.data=[e.data])),u(e.fields||[],e.data||[],i);throw new Error("Unable to serialize unrecognized input");function h(e){if("object"!=typeof e)return[];var t=[];for(var i in e)t.push(i);return t}function u(e,t,i){var r="";"string"==typeof e&&(e=JSON.parse(e)),"string"==typeof t&&(t=JSON.parse(t));var n=Array.isArray(e)&&0=this._config.preview;if(o)f.postMessage({results:n,workerId:b.WORKER_ID,finished:a});else if(U(this._config.chunk)&&!t){if(this._config.chunk(n,this._handle),this._handle.paused()||this._handle.aborted())return void(this._halted=!0);n=void 0,this._completeResults=void 0}return this._config.step||this._config.chunk||(this._completeResults.data=this._completeResults.data.concat(n.data),this._completeResults.errors=this._completeResults.errors.concat(n.errors),this._completeResults.meta=n.meta),this._completed||!a||!U(this._config.complete)||n&&n.meta.aborted||(this._config.complete(this._completeResults,this._input),this._completed=!0),a||n&&n.meta.paused||this._nextChunk(),n}this._halted=!0},this._sendError=function(e){U(this._config.error)?this._config.error(e):o&&this._config.error&&f.postMessage({workerId:b.WORKER_ID,error:e,finished:!1})}}function l(e){var r;(e=e||{}).chunkSize||(e.chunkSize=b.RemoteChunkSize),u.call(this,e),this._nextChunk=n?function(){this._readChunk(),this._chunkLoaded()}:function(){this._readChunk()},this.stream=function(e){this._input=e,this._nextChunk()},this._readChunk=function(){if(this._finished)this._chunkLoaded();else{if(r=new XMLHttpRequest,this._config.withCredentials&&(r.withCredentials=this._config.withCredentials),n||(r.onload=y(this._chunkLoaded,this),r.onerror=y(this._chunkError,this)),r.open(this._config.downloadRequestBody?"POST":"GET",this._input,!n),this._config.downloadRequestHeaders){var e=this._config.downloadRequestHeaders;for(var t in e)r.setRequestHeader(t,e[t])}if(this._config.chunkSize){var i=this._start+this._config.chunkSize-1;r.setRequestHeader("Range","bytes="+this._start+"-"+i)}try{r.send(this._config.downloadRequestBody)}catch(e){this._chunkError(e.message)}n&&0===r.status&&this._chunkError()}},this._chunkLoaded=function(){4===r.readyState&&(r.status<200||400<=r.status?this._chunkError():(this._start+=this._config.chunkSize?this._config.chunkSize:r.responseText.length,this._finished=!this._config.chunkSize||this._start>=function(e){var t=e.getResponseHeader("Content-Range");if(null===t)return-1;return parseInt(t.substring(t.lastIndexOf("/")+1))}(r),this.parseChunk(r.responseText)))},this._chunkError=function(e){var t=r.statusText||e;this._sendError(new Error(t))}}function c(e){var r,n;(e=e||{}).chunkSize||(e.chunkSize=b.LocalChunkSize),u.call(this,e);var s="undefined"!=typeof FileReader;this.stream=function(e){this._input=e,n=e.slice||e.webkitSlice||e.mozSlice,s?((r=new FileReader).onload=y(this._chunkLoaded,this),r.onerror=y(this._chunkError,this)):r=new FileReaderSync,this._nextChunk()},this._nextChunk=function(){this._finished||this._config.preview&&!(this._rowCount=this._input.size,this.parseChunk(e.target.result)},this._chunkError=function(){this._sendError(r.error)}}function p(e){var i;u.call(this,e=e||{}),this.stream=function(e){return i=e,this._nextChunk()},this._nextChunk=function(){if(!this._finished){var e,t=this._config.chunkSize;return t?(e=i.substring(0,t),i=i.substring(t)):(e=i,i=""),this._finished=!i,this.parseChunk(e)}}}function g(e){u.call(this,e=e||{});var t=[],i=!0,r=!1;this.pause=function(){u.prototype.pause.apply(this,arguments),this._input.pause()},this.resume=function(){u.prototype.resume.apply(this,arguments),this._input.resume()},this.stream=function(e){this._input=e,this._input.on("data",this._streamData),this._input.on("end",this._streamEnd),this._input.on("error",this._streamError)},this._checkIsFinished=function(){r&&1===t.length&&(this._finished=!0)},this._nextChunk=function(){this._checkIsFinished(),t.length?this.parseChunk(t.shift()):i=!0},this._streamData=y(function(e){try{t.push("string"==typeof e?e:e.toString(this._config.encoding)),i&&(i=!1,this._checkIsFinished(),this.parseChunk(t.shift()))}catch(e){this._streamError(e)}},this),this._streamError=y(function(e){this._streamCleanUp(),this._sendError(e)},this),this._streamEnd=y(function(){this._streamCleanUp(),r=!0,this._streamData("")},this),this._streamCleanUp=y(function(){this._input.removeListener("data",this._streamData),this._input.removeListener("end",this._streamEnd),this._input.removeListener("error",this._streamError)},this)}function i(m){var a,o,h,r=Math.pow(2,53),n=-r,s=/^\s*-?(\d+\.?|\.\d+|\d+\.\d+)(e[-+]?\d+)?\s*$/,u=/(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))/,t=this,i=0,f=0,d=!1,e=!1,l=[],c={data:[],errors:[],meta:{}};if(U(m.step)){var p=m.step;m.step=function(e){if(c=e,_())g();else{if(g(),0===c.data.length)return;i+=e.data.length,m.preview&&i>m.preview?o.abort():(c.data=c.data[0],p(c,t))}}}function v(e){return"greedy"===m.skipEmptyLines?""===e.join("").trim():1===e.length&&0===e[0].length}function g(){if(c&&h&&(k("Delimiter","UndetectableDelimiter","Unable to auto-detect delimiting character; defaulted to '"+b.DefaultDelimiter+"'"),h=!1),m.skipEmptyLines)for(var e=0;e=l.length?"__parsed_extra":l[i]),m.transform&&(s=m.transform(s,n)),s=y(n,s),"__parsed_extra"===n?(r[n]=r[n]||[],r[n].push(s)):r[n]=s}return m.header&&(i>l.length?k("FieldMismatch","TooManyFields","Too many fields: expected "+l.length+" fields but parsed "+i,f+t):i=r.length/2?"\r\n":"\r"}(e,r)),h=!1,m.delimiter)U(m.delimiter)&&(m.delimiter=m.delimiter(e),c.meta.delimiter=m.delimiter);else{var n=function(e,t,i,r,n){var s,a,o,h;n=n||[",","\t","|",";",b.RECORD_SEP,b.UNIT_SEP];for(var u=0;u=L)return R(!0)}else for(m=M,M++;;){if(-1===(m=a.indexOf(O,m+1)))return i||u.push({type:"Quotes",code:"MissingQuotes",message:"Quoted field unterminated",row:h.length,index:M}),E();if(m===r-1)return E(a.substring(M,m).replace(_,O));if(O!==z||a[m+1]!==z){if(O===z||0===m||a[m-1]!==z){-1!==p&&p=L)return R(!0);break}u.push({type:"Quotes",code:"InvalidQuotes",message:"Trailing quote on quoted field is malformed",row:h.length,index:M}),m++}}else m++}return E();function b(e){h.push(e),d=M}function w(e){var t=0;if(-1!==e){var i=a.substring(m+1,e);i&&""===i.trim()&&(t=i.length)}return t}function E(e){return i||(void 0===e&&(e=a.substring(M)),f.push(e),M=r,b(f),o&&S()),R()}function C(e){M=e,b(f),f=[],g=a.indexOf(I,M)}function R(e){return{data:h,errors:u,meta:{delimiter:D,linebreak:I,aborted:j,truncated:!!e,cursor:d+(t||0)}}}function S(){A(R()),h=[],u=[]}function x(e,t,i){var r={nextDelim:void 0,quoteSearch:void 0},n=a.indexOf(O,t+1);if(t=this._config.preview;if(o)f.postMessage({results:n,workerId:b.WORKER_ID,finished:a});else if(U(this._config.chunk)&&!t){if(this._config.chunk(n,this._handle),this._handle.paused()||this._handle.aborted())return void(this._halted=!0);n=void 0,this._completeResults=void 0}return this._config.step||this._config.chunk||(this._completeResults.data=this._completeResults.data.concat(n.data),this._completeResults.errors=this._completeResults.errors.concat(n.errors),this._completeResults.meta=n.meta),this._completed||!a||!U(this._config.complete)||n&&n.meta.aborted||(this._config.complete(this._completeResults,this._input),this._completed=!0),a||n&&n.meta.paused||this._nextChunk(),n}this._halted=!0},this._sendError=function(e){U(this._config.error)?this._config.error(e):o&&this._config.error&&f.postMessage({workerId:b.WORKER_ID,error:e,finished:!1})}}function l(e){var r;(e=e||{}).chunkSize||(e.chunkSize=b.RemoteChunkSize),u.call(this,e),this._nextChunk=n?function(){this._readChunk(),this._chunkLoaded()}:function(){this._readChunk()},this.stream=function(e){this._input=e,this._nextChunk()},this._readChunk=function(){if(this._finished)this._chunkLoaded();else{if(r=new XMLHttpRequest,this._config.withCredentials&&(r.withCredentials=this._config.withCredentials),n||(r.onload=y(this._chunkLoaded,this),r.onerror=y(this._chunkError,this)),r.open(this._config.downloadRequestBody?"POST":"GET",this._input,!n),this._config.downloadRequestHeaders){var e=this._config.downloadRequestHeaders;for(var t in e)r.setRequestHeader(t,e[t])}if(this._config.chunkSize){var i=this._start+this._config.chunkSize-1;r.setRequestHeader("Range","bytes="+this._start+"-"+i)}try{r.send(this._config.downloadRequestBody)}catch(e){this._chunkError(e.message)}n&&0===r.status&&this._chunkError()}},this._chunkLoaded=function(){4===r.readyState&&(r.status<200||400<=r.status?this._chunkError():(this._start+=this._config.chunkSize?this._config.chunkSize:r.responseText.length,this._finished=!this._config.chunkSize||this._start>=function(e){var t=e.getResponseHeader("Content-Range");if(null===t)return-1;return parseInt(t.substring(t.lastIndexOf("/")+1))}(r),this.parseChunk(r.responseText)))},this._chunkError=function(e){var t=r.statusText||e;this._sendError(new Error(t))}}function c(e){var r,n;(e=e||{}).chunkSize||(e.chunkSize=b.LocalChunkSize),u.call(this,e);var s="undefined"!=typeof FileReader;this.stream=function(e){this._input=e,n=e.slice||e.webkitSlice||e.mozSlice,s?((r=new FileReader).onload=y(this._chunkLoaded,this),r.onerror=y(this._chunkError,this)):r=new FileReaderSync,this._nextChunk()},this._nextChunk=function(){this._finished||this._config.preview&&!(this._rowCount=this._input.size,this.parseChunk(e.target.result)},this._chunkError=function(){this._sendError(r.error)}}function p(e){var i;u.call(this,e=e||{}),this.stream=function(e){return i=e,this._nextChunk()},this._nextChunk=function(){if(!this._finished){var e,t=this._config.chunkSize;return t?(e=i.substring(0,t),i=i.substring(t)):(e=i,i=""),this._finished=!i,this.parseChunk(e)}}}function g(e){u.call(this,e=e||{});var t=[],i=!0,r=!1;this.pause=function(){u.prototype.pause.apply(this,arguments),this._input.pause()},this.resume=function(){u.prototype.resume.apply(this,arguments),this._input.resume()},this.stream=function(e){this._input=e,this._input.on("data",this._streamData),this._input.on("end",this._streamEnd),this._input.on("error",this._streamError)},this._checkIsFinished=function(){r&&1===t.length&&(this._finished=!0)},this._nextChunk=function(){this._checkIsFinished(),t.length?this.parseChunk(t.shift()):i=!0},this._streamData=y(function(e){try{t.push("string"==typeof e?e:e.toString(this._config.encoding)),i&&(i=!1,this._checkIsFinished(),this.parseChunk(t.shift()))}catch(e){this._streamError(e)}},this),this._streamError=y(function(e){this._streamCleanUp(),this._sendError(e)},this),this._streamEnd=y(function(){this._streamCleanUp(),r=!0,this._streamData("")},this),this._streamCleanUp=y(function(){this._input.removeListener("data",this._streamData),this._input.removeListener("end",this._streamEnd),this._input.removeListener("error",this._streamError)},this)}function i(m){var a,o,h,r=Math.pow(2,53),n=-r,s=/^\s*-?(\d+\.?|\.\d+|\d+\.\d+)(e[-+]?\d+)?\s*$/,u=/(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))/,t=this,i=0,f=0,d=!1,e=!1,l=[],c={data:[],errors:[],meta:{}};if(U(m.step)){var p=m.step;m.step=function(e){if(c=e,_())g();else{if(g(),0===c.data.length)return;i+=e.data.length,m.preview&&i>m.preview?o.abort():(c.data=c.data[0],p(c,t))}}}function v(e){return"greedy"===m.skipEmptyLines?""===e.join("").trim():1===e.length&&0===e[0].length}function g(){if(c&&h&&(k("Delimiter","UndetectableDelimiter","Unable to auto-detect delimiting character; defaulted to '"+b.DefaultDelimiter+"'"),h=!1),m.skipEmptyLines)for(var e=0;e=l.length?"__parsed_extra":l[i]),m.transform&&(s=m.transform(s,n)),s=y(n,s),"__parsed_extra"===n?(r[n]=r[n]||[],r[n].push(s)):r[n]=s}return m.header&&(i>l.length?k("FieldMismatch","TooManyFields","Too many fields: expected "+l.length+" fields but parsed "+i,f+t):i=r.length/2?"\r\n":"\r"}(e,r)),h=!1,m.delimiter)U(m.delimiter)&&(m.delimiter=m.delimiter(e),c.meta.delimiter=m.delimiter);else{var n=function(e,t,i,r,n){var s,a,o,h;n=n||[",","\t","|",";",b.RECORD_SEP,b.UNIT_SEP];for(var u=0;u=L)return R(!0)}else for(m=M,M++;;){if(-1===(m=a.indexOf(O,m+1)))return i||u.push({type:"Quotes",code:"MissingQuotes",message:"Quoted field unterminated",row:h.length,index:M}),E();if(m===r-1)return E(a.substring(M,m).replace(_,O));if(O!==z||a[m+1]!==z){if(O===z||0===m||a[m-1]!==z){-1!==p&&p=L)return R(!0);break}u.push({type:"Quotes",code:"InvalidQuotes",message:"Trailing quote on quoted field is malformed",row:h.length,index:M}),m++}}else m++}return E();function b(e){h.push(e),d=M}function w(e){var t=0;if(-1!==e){var i=a.substring(m+1,e);i&&""===i.trim()&&(t=i.length)}return t}function E(e){return i||(void 0===e&&(e=a.substring(M)),f.push(e),M=r,b(f),o&&S()),R()}function C(e){M=e,b(f),f=[],g=a.indexOf(I,M)}function R(e){return{data:h,errors:u,meta:{delimiter:D,linebreak:I,aborted:j,truncated:!!e,cursor:d+(t||0)}}}function S(){A(R()),h=[],u=[]}function x(e,t,i){var r={nextDelim:void 0,quoteSearch:void 0},n=a.indexOf(O,t+1);if(t