├── .eslintrc.json ├── .gitignore ├── .jshintrc ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── .vscodeignore ├── CHANGELOG.md ├── LICENSE ├── MANUAL_TESTS.md ├── PUBLISHING.md ├── README.md ├── bl └── netSuiteBl.js ├── extension.js ├── helpers ├── codeChangeHelper.js ├── netSuiteRestClient.js ├── netsuiteList.js └── uiHelper.js ├── img ├── icon.png ├── netsuite_upload.gif └── snippet_addModule.gif ├── netSuiteRestlet └── vscodeExtensionRestlet.js ├── package-lock.json ├── package.json ├── snippets └── snippets.json └── test ├── extension.test.js └── index.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": false, 4 | "commonjs": true, 5 | "es6": true, 6 | "node": true 7 | }, 8 | "parserOptions": { 9 | "ecmaFeatures": { 10 | "jsx": true 11 | }, 12 | "sourceType": "module" 13 | }, 14 | "rules": { 15 | "no-const-assign": "warn", 16 | "no-this-before-super": "warn", 17 | "no-undef": "warn", 18 | "no-unreachable": "warn", 19 | "no-unused-vars": "warn", 20 | "constructor-super": "warn", 21 | "valid-typeof": "warn" 22 | } 23 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.vsix 3 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esversion": 6 3 | } -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint" 6 | ] 7 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that launches the extension inside a new window 2 | { 3 | "version": "0.1.0", 4 | "configurations": [ 5 | { 6 | "name": "Launch Extension", 7 | "type": "extensionHost", 8 | "request": "launch", 9 | "runtimeExecutable": "${execPath}", 10 | "args": ["--extensionDevelopmentPath=${workspaceRoot}" ], 11 | "stopOnEntry": false 12 | }, 13 | { 14 | "name": "Launch Tests", 15 | "type": "extensionHost", 16 | "request": "launch", 17 | "runtimeExecutable": "${execPath}", 18 | "args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/test" ], 19 | "stopOnEntry": false 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | // we want to use the TS server from our node_modules folder to control its version 4 | "typescript.tsdk": "./node_modules/typescript/lib", 5 | 6 | // OAuth NetSuite Token ID 7 | "netSuiteUpload.netSuiteKey": "", 8 | 9 | // OAuth NetSuite Token Secret 10 | "netSuiteUpload.netSuiteSecret": "", 11 | 12 | // OAuth NetSuite Consumer Key 13 | "netSuiteUpload.consumerToken": "", 14 | 15 | // OAuth NetSuite Consumer Secret 16 | "netSuiteUpload.consumerSecret": "", 17 | 18 | // Account number 19 | "netSuiteUpload.realm": "", 20 | 21 | "editor.tabSize": 4, 22 | "javascript.format.enable": false, 23 | "editor.detectIndentation": true 24 | } -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | test/** 4 | .gitignore 5 | jsconfig.json 6 | vsc-extension-quickstart.md 7 | .eslintrc.json 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [1.2.4] - 2019-12-26 9 | 10 | - Closes Issue #31 "Add Netsuite dependency command doesn't work properly" https://github.com/netsuite-upload-org/netsuite-upload/issues/31 11 | 12 | ### Added 13 | 14 | - Added support for pushing a whole folder. Thank you @alejndr https://github.com/netsuite-upload-org/netsuite-upload/pull/35 15 | 16 | ## [1.2.2] - 2019-09-13 17 | 18 | ### Changed 19 | 20 | - Added support for uploading and downloading .ts TypeScript files. https://github.com/netsuite-upload-org/netsuite-upload/pull/32 21 | - Updated npm dependencies to eliminate security vulnerabilities. 22 | 23 | ## [1.1.2] - 2019-02-08 24 | 25 | ### Changed 26 | 27 | - Publishing to VS Code Marketplace under a new publisher name, `nsupload-org`. This will make it appear as a different extension than the old one. Going to remove the old extension from the VS Code Marketplace. 28 | - Added keybinding for upload. Ctrl+n,Ctrl+u. This complements the download keybinding, Ctrl+n,Ctrl+d. 29 | - Improved some Settings descriptions. 30 | 31 | ### Fixed 32 | 33 | - Checking the version of the RESTlet was too strict. I don't need the version of the Extension to equal the version of the RESTlet. I just need all the supported functions to work properly. 34 | 35 | ## [1.0.2] - 2019-02-07 36 | 37 | ### Fixed 38 | 39 | - Fixed OAuth support. NetSuite OAuth is weird. 40 | 41 | ### Added 42 | 43 | - Assigned a version to the RESTlet, and created a GET request that will pull down the version number of the RESTlet. This allows the extension to detect when the RESTlet version is not up-to-date, and to warn the user. There's also a new palette command, `Get NSUpload RESTlet version` which will fetch the value and display it in a notification. 44 | - Continued improving error handling. Now can detect bad authentication and warn the user. 45 | 46 | ### Changed 47 | 48 | - This release requires that you update the RESTlet in NetSuite. Find the RESTlet at `netSuiteRestlet\vscodeExtensionRestlet.js`. 49 | 50 | ## [1.0.1] - 2019-02-05 51 | 52 | ### Added 53 | 54 | - This release adds a feature requested by [@JonnyBoy333](https://github.com/JonnyBoy333). It allows for a setting to change the base folder path to upload scripts. For example, if you keep a copy of all scripts in SuiteScripts/Developer, then you can change this setting and push and pull files there. When you're done with development, you can change the setting back and push files to production. 55 | 56 | ### Changed 57 | 58 | - This release requires that you update the RESTlet in NetSuite. Find the RESTlet at `netSuiteRestlet\vscodeExtensionRestlet.js`. 59 | 60 | ## [1.0.0] - 2019-02-05 61 | 62 | Original author Tomáš Tvrdý [tvrdytom](https://github.com/tvrdytom) has turned over ownership of this project to me. I'm releasing an updated version 1.0 with many fixes. 63 | 64 | See the [readme.md](https://github.com/netsuite-upload-org/netsuite-upload) for install instructions. This version is not in the VS Code Marketplace yet. 65 | 66 | - Enabled pushing up the active document in the editor using a keybinding (I chose Ctrl+U, personally). 67 | - Improved cross-platform support (mac). Previously, Windows local file paths were assumed. 68 | - Now properly recognizes and sets the correct file type by inspecting the filename extension when uploading a new file to NetSuite. Supports all file extensions that are documented in NetSuite documentation. 69 | - Improved file vs. folder recognition in the left explorer pane. Previously, if a file type wasn't a type of code file that VS Code knew about (like a .csv file), the NetSuite context menu wouldn't show. 70 | - Improved messaging. More comprehensive messages will be shown in the VS Code "toast" notification when a file or folder operation succeeds or fails. 71 | - Stopped using node-rest-client and replaced with SuperAgent, easier to use. 72 | 73 | I attempted to add OAuth support. I'd appreciate if anyone would like to try a test. I couldn't get it working on my machine. Config instructions are in the README.md. 74 | 75 | *If you upgrade to this version, you **must** also upgrade the RESTlet `vscodeExtensionRestlet.js` in NetSuite.* 76 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Tomas Tvrdy 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 | -------------------------------------------------------------------------------- /MANUAL_TESTS.md: -------------------------------------------------------------------------------- 1 | # Manual Test Plan 2 | 3 | ## Test NLAuth Authentication 4 | 5 | Configure NLAuth authentication in Workspace settings. Comment out all OAuth fields. 6 | 7 | Run: Command Palette…NetSuite: Get RESTlet Version 8 | 9 | Expected Result: Window notification with current RESTlet version. 10 | 11 | ## Test OAuth Authentication 12 | 13 | Comment out NLAuth and configure OAuth parameters in Workspace settings. 14 | 15 | Run: Command Palette…NetSuite: Get RESTlet Version 16 | 17 | Expected Result: Window notification with current RESTlet version. 18 | 19 | ## Test file upload and remote folder creation 20 | 21 | Create a local folder that does not exist in NetSuite. 22 | 23 | Place 2 files in it. 24 | 25 | - Cannot be 0-length, as that fails. 26 | - One should be .txt 27 | - One should be .csv 28 | 29 | Push up the .txt. Should create the folder and the file in NetSuite 30 | 31 | Push up the .csv. Check that the file exists in NetSuite 32 | 33 | ## Test folder download 34 | 35 | Delete the .txt and .csv files from the folder created in the earlier step. 36 | 37 | Pull the directory from NetSuite 38 | 39 | Show that both files came down. 40 | 41 | ## Test file compare 42 | 43 | Modify the .txt file locally. 44 | 45 | Run a file comparison. See that the diff is shown. 46 | 47 | ## Test remote delete 48 | 49 | Right click the .csv file in the file explorer and choose "Delete file in NetSuite". Confirm it worked. 50 | 51 | Attempt to download the .csv file. Should produce an error message. 52 | 53 | ## Test changing the remote subfolder 54 | 55 | Change the rootDirectory setting in Workspace settings to some other folder where you have write access. Maybe a subfolder under SuiteScripts like SuiteScripts/Test (you might need to create this folder in NetSuite ahead of time). 56 | 57 | Push the .csv file to NetSuite. 58 | 59 | Check that the .csv file landed in the remote custom subfolder as expected. -------------------------------------------------------------------------------- /PUBLISHING.md: -------------------------------------------------------------------------------- 1 | # Publishing Process 2 | 3 | ## Making changes 4 | 5 | Create a branch, make changes in the branch. 6 | 7 | Update package.json as needed. Update the version number manually and consider updating any dependencies. 8 | 9 | Use `npm audit` to check for security vulnerabilies and `npm audit fix` to fix them, then test. 10 | 11 | Update `README.md` and `CHANGELOG.md` as needed. 12 | 13 | Commit the branch and open a PR for review. 14 | 15 | Review the code and when approved, merge to master. 16 | 17 | ## Build with VSCE 18 | 19 | Minor revision: 20 | 21 | ```powershell 22 | vsce package 23 | ``` 24 | 25 | ## Release in Github 26 | 27 | In Github, go to Releases tab. Create a new release. 28 | 29 | Click "Draft a new release". 30 | 31 | Specify the version tag for both "Tag version" and "Release title" like: v1.0.4 32 | 33 | Copy CHANGELOG.md details into the text area for "Describe this release." 34 | 35 | Drag the .vsix file outputted from `vsce package` into the "Attach binaries" area in the Github release. 36 | 37 | Click `Publish release` button. 38 | 39 | ## Release to VS Code Marketplace. 40 | 41 | Use same version number that you used in the `vsce package` step. 42 | 43 | ```powershell 44 | vsce publish 1.x.x 45 | ``` 46 | 47 | Package and publish to VS Code Marketplace. 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## netsuite-upload VS Code plugin 2 | 3 | [![Version](https://vsmarketplacebadge.apphb.com/version/nsupload-org.netsuite-upload.svg)](https://marketplace.visualstudio.com/items?itemName=nsupload-org.netsuite-upload) 4 | 5 | **netsuite-upload** is a Visual Studio Code extension that allows you to manage your SuiteScript files directly from VS Code. It also helps you with defining new modules and adding server-side module dependecies. 6 | 7 | ## Features 8 | 9 | ### 1. Push and Pull Files and Folders between VS Code and the NetSuite File Cabinet 10 | 11 | Right-click a file or folder in the navigation panel to see the context menu options: 12 | 13 | - `Pull file from NetSuite` - downloads a file from NetSuite 14 | - `Push file to NetSuite` - uploads a file to NetSuite 15 | - `Delete file in NetSuite` - deletes a file in NetSuite 16 | - `Compare file with NetSuite` - diff your local version with the NetSuite version 17 | - `Pull folder from NetSuite` - Download the folder and all contents from NetSuite 18 | - `Push folder to NetSuite` - Uploads the folder and all contents to NetSuite 19 | 20 | ![Snippet & commands](img/netsuite_upload.gif) 21 | 22 | ### 2. NetSuite-Specific Code Snippets & Commands 23 | 24 | - `Snippets for module initialization` - type _defineRestlet..._, choose your module type and hit enter 25 | - `Commands for adding new NetSuite/custom dependencies` - open command line (`Ctrl`-`Shift`-`P`) and type 26 | - _add netsuite dependency_ for choosing of the NetSuite built-in module from the list 27 | - _add custom dependency_ for defining od custom dependecies 28 | 29 | ![Snippet & commands](img/snippet_addModule.gif) 30 | 31 | ### 3. Rebase the Root Folder 32 | 33 | You can set the remote destination folder in NetSuite to SuiteScripts (default) or a subfolder of SuiteScripts with the `netSuiteUpload.rootDirectory` setting. This might be useful if you have a folder in the NS file cabinet called `SuiteScripts\Development` where you experiment with scripts before you move them to a `SuiteScripts\Production` folder. 34 | 35 | In this kind of work flow, you might do development work within `SuiteScripts\Development`. When you're satisfied, you'd change Settings.json (or Workspace settings in your `.vscode\settings.json` file) to change the base to `SuiteScripts\Production`. Then you'd push up all the files you were working with. 36 | 37 | Nothing changes on your local disk due to this setting change. It only modifies the root folder in NetSuite where files get pushed to or pulled from. 38 | 39 | This was a requested enhancement. Most people would probably be better off using a sandbox environment as it would be easy to lose track of which files were modified, and which versions of which files were in Development vs Production. 40 | 41 | ## NetSuite Setup 42 | 43 | To be able to push and pull folders and files to NetSuite, this extension requires the manual installation of a RESTlet in NetSuite. Through configuration settings, you will inform the extension as to the URL of the RESTlet. 44 | 45 | ### How to install the RESTlet 46 | 47 | You'll need to know how to publish a script and do a script deployment in NetSuite to make it work. Consult the NetSuite docs if this is new to you. 48 | 49 | - [Download a copy of the RESTlet from here](https://github.com/netsuite-upload-org/netsuite-upload/blob/master/netSuiteRestlet/vscodeExtensionRestlet.js) (use `Raw` view to copy/paste). 50 | - Upload the `vscodeExtensionRestlet.js` file to somewhere in your `SuiteScripts` file cabinet folder in NetSuite. 51 | - Create a new Script record for this RESTlet. 52 | - Create a new Script Deployment for this Script. Note the URL. 53 | - Edit your workspace or user settings in VS Code (see Settings section below) and set the RESTlet URL. 54 | 55 | ### Special Notice Regarding the RESTlet 56 | 57 | - Future versions of this VS Code Extension may require that you upgrade the RESTlet file in NetSuite. Take note if the extension receives an update, and read the Changelog. 58 | - If you have an old version of the RESTlet in your Production NetSuite instance, and a new version in a SandBox NetSuite instance, be warned. A SandBox refresh might overwrite the SandBox RESTlet with the older version from Production. 59 | 60 | ## VS Code Setup 61 | 62 | ### Install the VS Code Extension 63 | 64 | The extension can be installed from the VS Code Extensions Marketplace within VS Code. 65 | 66 | The Marketplace URL is https://marketplace.visualstudio.com/items?itemName=nsupload-org.netsuite-upload 67 | 68 | ### VSCode - Open Folder 69 | 70 | **Very Important Fact** - Your VS Code project **MUST MUST MUST** be rooted at a folder that maps or corresponds to NetSuite's "SuiteScripts" file cabinet folder. This extension assumes the working root is equivalent to the remote "SuiteScripts" folder. 71 | 72 | In VS Code, open the folder that corresponds to your local copy of the **SuiteScripts** folder in VSCode. That folder may not be named "SuiteScripts", but it should be the folder that _corresponds_ to SuiteScripts in the NetSuite file cabiner. 73 | 74 | ### VS Code Extension Settings 75 | 76 | You may manage settings for this extension either in your global user settings for all VS Code projects, or in a project-specific Workspace settings file that can be created or found beneath `.vscode\settings.json`. 77 | 78 | Keep reading for required settings relating to authentication and more. 79 | 80 | ## Authentication Setup 81 | 82 | This extension can authenticate to NetSuite and access the RESTlet script using either NLAuth authorization or OAuth 1.0 authorization. 83 | 84 | ### Authentication Option 1: NLAuth Authorization 85 | 86 | Place the following in either Workspace settings or general User settings: 87 | 88 | ```javascript 89 | { 90 | // Authentication header 91 | "netSuiteUpload.authentication": "NLAuth nlauth_account=, nlauth_email=, nlauth_signature=, nlauth_role=", 92 | 93 | } 94 | ``` 95 | 96 | Where: 97 | 98 | - ACCOUNTID is your NetSuite account ID number 99 | - LOGIN is your email address used to log into NetSuite 100 | - PASSWORD is your password (make sure you don't commit this file to source control) 101 | - ROLE is the numeric NetSuite RoleID for which you have web service/API permissions. You may need to go look this up in NetSuite Setup…Users/Roles…Manage Roles. 102 | 103 | ### Authentication Option 2: OAuth 104 | 105 | Generating the necessary tokens for OAuth is covered in the NetSuite help site. It's not fun. 106 | 107 | - If you wish to use OAuth authentication instead of basic authentication you can leave the authentication header blank and use the OAuth settings properties. 108 | - First, generate an Integration record in NetSuite, make sure the 'token based authentication' scheme is checked, and hang on to the token and secret details. 109 | - Second, log into a role you wish to use for authentication. From the "manage tokens center", generate a new token and secret using the Integration from the previous step. 110 | - Input the 4 values from above (NetSuite key and token and Consumer key and token) in the corresponding settings options. 111 | - Set the `realm` setting equal to your numeric NetSuite account number. 112 | 113 | ## Authorization - Required Role Permissions 114 | 115 | This extension requires the use of a NetSuite RESTlet. That RESTlet will be manipulating files and folders in the SuiteScripts folder of the File Cabinet. Therefore, the user being authenticated must have sufficient permissions assigned to their Role to allow these file changes, and to call the RESTlet script deployment. 116 | 117 | At a minimum, the Role must have the following permissions: 118 | 119 | - Lists…Documents and Files: Full 120 | - Setup…Allow JS / HTML Uploads 121 | - Setup…Log in using Access Tokens: Full 122 | - Setup…SuiteScript: Full 123 | 124 | 125 | If you wish to upload and download into the **SuiteBundles folder** by changing the `rootDirectory` setting, add the following permission to your Role. 126 | 127 | - Setup…SuiteBundler: Full 128 | 129 | ## settings.json 130 | 131 | The following demonstrates all possible settings. 132 | 133 | ```javascript 134 | { 135 | // Script Deployment URL for the deployed vscodeExtensionRestlet.js 136 | "netSuiteUpload.restlet": "", 137 | 138 | // Base NetSuite folder path to upload script to (e.g. "SuiteScripts/Developer"). Default if unset is "SuiteScripts". 139 | "netSuiteUpload.rootDirectory": "" 140 | 141 | // Temporary folder (e.g. C:\\temp or /tmp) - used for diffing files between local and remote. 142 | "netSuiteUpload.tempFolder": "" 143 | 144 | // AUTHENTICATION - Use either NLAuth or Token Auth. 145 | 146 | // Authentication header for NLAuth. remove or comment out if using OAuth. 147 | "netSuiteUpload.authentication": "NLAuth nlauth_account=, nlauth_email=, nlauth_signature=, nlauth_role=", 148 | 149 | // If using OAuth, set all of these. 150 | // Oauth NetSuite Key or Token ID 151 | "netSuiteUpload.netSuiteKey": "", 152 | // Oauth NetSuite Secret 153 | "netSuiteUpload.netSuiteSecret": "", 154 | // Oauth NetSuite Consumer Key 155 | "netSuiteUpload.consumerToken": "", 156 | // Oauth NetSuite Consumer Secret 157 | "netSuiteUpload.consumerSecret": "", 158 | // Account number 159 | "netSuiteUpload.realm": "", 160 | } 161 | ``` 162 | 163 | ## keybindings.json 164 | 165 | You can add keybindings for a number of operations. 166 | 167 | By default, two keybindings are pre-set in the Extension: upload and download. 168 | 169 | - Upload: Ctrl+n,Ctrl+u 170 | - Download: Ctrl+n,Ctrl+d 171 | 172 | You can remap or set new like so in your `keybindings.json` file: 173 | 174 | ```javascript 175 | { "key": "ctrl+u", "command": "netsuite-upload.uploadFile"}, 176 | ``` 177 | 178 | ## Known Issues and Limitations 179 | 180 | The plugin is using a RESTlet for the communication with NetSuite. RESTlets have some governance limitations, meaning NetSuite may throttle API calls if they are sent too rapidly. The current implementation does not deal with this, so there could be problems pulling folders containing a lot of items from NetSuite. 181 | -------------------------------------------------------------------------------- /bl/netSuiteBl.js: -------------------------------------------------------------------------------- 1 | let vscode = require('vscode'); 2 | let fs = require('fs'); 3 | let path = require('path'); 4 | let nsRestClient = require('../helpers/netSuiteRestClient'); 5 | let codeChangeHelper = require('../helpers/codeChangeHelper'); 6 | let uiHelper = require('../helpers/uiHelper'); 7 | let netsuiteList = require('../helpers/netsuiteList'); 8 | let _ = require('underscore'); 9 | 10 | function getRestletVersion() { 11 | nsRestClient.getRestletVersion(function (err, res) { 12 | if (hasNetSuiteError("Could not determine the version of the RESTlet deployed to NetSuite. Are you sure you have uploaded the most recent version of the RESTlet 'vscodeExtensionRestlet.js'?", err, res)) { 13 | return; 14 | } 15 | vscode.window.showInformationMessage('RESTlet version is ' + res.body.restletVersion); 16 | }); 17 | } 18 | 19 | function downloadFileFromNetSuite(file) { 20 | nsRestClient.getFile(file, function (err, res) { 21 | if (hasNetSuiteError('ERROR downloading file.', err, res)) return; 22 | 23 | var relativeFileName = nsRestClient.getRelativePath(file.fsPath); 24 | fs.writeFile(file.fsPath, res.body[0].content, (err) => { 25 | if (err) throw err; 26 | }); 27 | 28 | vscode.window.showInformationMessage('SUCCESS! File "' + relativeFileName + '" downloaded.'); 29 | }); 30 | } 31 | 32 | function uploadFileToNetSuite(file) { 33 | var fileContent = fs.readFileSync(file.fsPath, 'utf8'); 34 | 35 | nsRestClient.postFile(file, fileContent, function (err, res) { 36 | if (hasNetSuiteError('ERROR uploading file.', err, res)) return; 37 | 38 | var relativeFileName = nsRestClient.getRelativePath(file.fsPath); 39 | vscode.window.showInformationMessage('SUCCESS! File "' + relativeFileName + '" uploaded.'); 40 | }); 41 | } 42 | 43 | function hasNetSuiteError(custommessage, err, response) { 44 | if (err) { 45 | var get = function (obj, key) { 46 | return key.split('.').reduce(function (o, x) { 47 | return (typeof o == 'undefined' || o === null) ? o : o[x]; 48 | }, obj); 49 | }; 50 | 51 | var errorDetails = []; 52 | if (response && get(response, 'status') === 403) { // Forbidden. Bad Auth. 53 | errorDetails = [ 54 | 'AUTHENTICATION FAILED!', 55 | 'HTTP Status: 403', 56 | 'HTTP Error: ' + get(response, 'message'), 57 | 'Local Stack:', 58 | get(response, 'stack') 59 | ]; 60 | } else if (err.shortmessage) { 61 | // We passed in a simple, short message which is all we need to display. 62 | errorDetails = [err.shortmessage]; 63 | 64 | } else if (response && response.body && response.body.error) { 65 | // The body of the response may contain a JSON object containing a NetSuite-specific 66 | // message. We'll parse and display that in addition to the HTTP message. 67 | try { 68 | 69 | var nsErrorObj = JSON.parse(response.body.error.message); 70 | 71 | if (nsErrorObj.name === 'SSS_MISSING_REQD_ARGUMENT') { 72 | custommessage += ' NetSuite N/file module does not allow storing an empty file.'; 73 | } 74 | 75 | errorDetails = [ 76 | 'NetSuite Error Details:', 77 | get(nsErrorObj, 'type'), 78 | get(nsErrorObj, 'name'), 79 | get(nsErrorObj, 'message'), 80 | get(nsErrorObj, 'code'), 81 | 'Remote Stack:', 82 | get(nsErrorObj, 'stack'), 83 | 'HTTP Status: ' + get(err, 'status'), 84 | 'HTTP Error: ' + get(err, 'message'), 85 | 'Local Stack:', 86 | get(err, 'stack') 87 | ]; 88 | } catch (e) { 89 | // Response body error does not contain a JSON message. 90 | errorDetails = [ 91 | 'NetSuite Error Details:', 92 | 'NS Error: ' + get(response.body.error, 'code'), 93 | 'NS Message: ' + get(response.body.error, 'message'), 94 | 'HTTP Status: ' + get(err, 'status'), 95 | 'HTTP Error: ' + get(err, 'message'), 96 | 'Local Stack:', 97 | get(err, 'stack') 98 | ]; 99 | } 100 | } else { 101 | errorDetails = [ 102 | 'Unknown Error:', 103 | 'HTTP Status: ' + get(err, 'status'), 104 | 'HTTP Error: ' + get(err, 'message'), 105 | 'Local Stack:', 106 | get(err, 'stack') 107 | ]; 108 | } 109 | 110 | // Pre-pend the custommessage and our own message. 111 | errorDetails.unshift(custommessage); 112 | errorDetails.push('Use Help…Toggle Developer Tools and choose the Console tab for a better formatted error message.'); 113 | console.log(errorDetails.join('\n')); 114 | // vscode window doesn't support newlines. 115 | vscode.window.showErrorMessage(errorDetails.join(' | ')); 116 | 117 | return true; 118 | } 119 | return false; 120 | } 121 | 122 | function deleteFileInNetSuite(file) { 123 | nsRestClient.deleteFile(file, function (err, res) { 124 | if (hasNetSuiteError('ERROR deleting file.', err, res)) return; 125 | var relativeFileName = nsRestClient.getRelativePath(file.fsPath); 126 | vscode.window.showInformationMessage('SUCCESS! Deleted file "' + relativeFileName + '".'); 127 | }); 128 | } 129 | 130 | function previewFileFromNetSuite(file) { 131 | nsRestClient.getFile(file, function (err, res) { 132 | if (hasNetSuiteError('ERROR downloading file.', err, res)) return; 133 | 134 | var relativeFileName = nsRestClient.getRelativePath(file.fsPath); 135 | var tempFolder = vscode.workspace.getConfiguration('netSuiteUpload').tempFolder; 136 | var filePathArray = (relativeFileName.split('.')[0] + '.preview.' + relativeFileName.split('.')[1]).split(path.sep); 137 | var newPreviewFile = path.join(tempFolder, filePathArray[filePathArray.length - 1]); 138 | 139 | fs.writeFile(newPreviewFile, res.body[0].content, (err) => { 140 | if (err) throw err; 141 | }); 142 | 143 | var nsFile = vscode.Uri.file(newPreviewFile); 144 | vscode.commands.executeCommand('vscode.diff', file, nsFile, 'Local <--> NetSuite'); 145 | }); 146 | } 147 | 148 | function downloadDirectoryFromNetSuite(directory) { 149 | nsRestClient.getDirectory(directory, function (err, res) { 150 | if (hasNetSuiteError('ERROR downloading directory.', err, res)) return; 151 | 152 | res.body.forEach(function (file) { 153 | var fullFilePath = path.join(vscode.workspace.rootPath, file.fullPath.replace(/^SuiteScripts\//, '').split('/').join(path.sep)); 154 | 155 | createDirectoryIfNotExist(fullFilePath + (file.type == 'folder' ? path.sep + '_' : '')); 156 | 157 | if (file.type === 'file') { 158 | fs.writeFile(fullFilePath, file.content, (err) => { 159 | if (err) throw err; 160 | }); 161 | } 162 | }); 163 | 164 | vscode.window.showInformationMessage('SUCCESS: Downloaded ' + res.body.length + ' file(s).'); 165 | }); 166 | } 167 | 168 | function createDirectoryIfNotExist(filePath) { 169 | var dirname = path.dirname(filePath); 170 | 171 | if (fs.existsSync(dirname)) { 172 | return true; 173 | } 174 | 175 | createDirectoryIfNotExist(dirname); 176 | fs.mkdirSync(dirname); 177 | } 178 | 179 | function addCustomDependencyToActiveFile(editor) { 180 | uiHelper.askForCustomDependency() 181 | .then(values => { 182 | addDependency(editor, values.depPath, values.depParam); 183 | }); 184 | } 185 | 186 | function addNetSuiteDependencyToActiveFile(editor) { 187 | let netsuiteLibs = netsuiteList.getSuiteScriptDependecies(); 188 | 189 | uiHelper.showListOfNetSuiteDependecies(_.pluck(netsuiteLibs, 'path')) 190 | .then(value => { 191 | var depRecord = _.findWhere(netsuiteLibs, { 192 | path: value 193 | }); 194 | addDependency(editor, depRecord.path, depRecord.param); 195 | }); 196 | } 197 | 198 | async function addDependency(editor, pathText, paramText) { 199 | let docContent = editor.document.getText(); 200 | let coords = codeChangeHelper.getCoords(docContent); 201 | let oldParamsString = docContent.substring(coords.depParam.range[0], coords.depParam.range[1]); 202 | 203 | let newParamsString = codeChangeHelper.getUpdatedFunctionParams(paramText, oldParamsString); 204 | let newPathArrayString = codeChangeHelper.getUpdatedDepPath(pathText, 205 | coords.depPath ? docContent.substring(coords.depPath.range[0], coords.depPath.range[1]) : null); 206 | 207 | if (coords.depPath) { 208 | await codeChangeHelper.updateDocument(editor, coords.depParam.start.row - 1, coords.depParam.start.col, 209 | coords.depParam.end.row - 1, coords.depParam.end.col, newParamsString); 210 | 211 | await codeChangeHelper.updateDocument(editor, coords.depPath.start.row - 1, coords.depPath.start.col, 212 | coords.depPath.end.row - 1, coords.depPath.end.col, newPathArrayString); 213 | } else { // Path array not defined 214 | await codeChangeHelper.updateDocument(editor, coords.depParam.start.row - 1, coords.depParam.start.col, 215 | coords.depParam.end.row - 1, coords.depParam.end.col, newPathArrayString + ', ' + newParamsString); 216 | } 217 | } 218 | 219 | function uploadDirectoryToNetSuite(directory) { 220 | // TODO: If needed add extension setting for exclude pattern 221 | vscode.workspace.findFiles(new vscode.RelativePattern(directory.path, '*.*')).then(files => files.forEach(file => uploadFileToNetSuite(file))); 222 | } 223 | 224 | exports.uploadDirectoryToNetSuite = uploadDirectoryToNetSuite; 225 | exports.downloadFileFromNetSuite = downloadFileFromNetSuite; 226 | exports.previewFileFromNetSuite = previewFileFromNetSuite; 227 | exports.downloadDirectoryFromNetSuite = downloadDirectoryFromNetSuite; 228 | exports.uploadFileToNetSuite = uploadFileToNetSuite; 229 | exports.deleteFileInNetSuite = deleteFileInNetSuite; 230 | exports.addCustomDependencyToActiveFile = addCustomDependencyToActiveFile; 231 | exports.addNetSuiteDependencyToActiveFile = addNetSuiteDependencyToActiveFile; 232 | exports.getRestletVersion = getRestletVersion; 233 | -------------------------------------------------------------------------------- /extension.js: -------------------------------------------------------------------------------- 1 | let vscode = require('vscode'); 2 | let netSuiteBl = require('./bl/netSuiteBl'); 3 | 4 | function activate(context) { 5 | console.log('Extension "netsuite-upload" is now active!'); 6 | 7 | let noProjectOpenedErrorMessage = 'No project is opened. Please open root folder. (SuiteScripts)'; 8 | let noFileSelectedErrorMessage = 'No file selected. Please right-click the file and select action from context menu.'; 9 | 10 | let downloadFileDisposable = vscode.commands.registerCommand('netsuite-upload.downloadFile', (file) => { 11 | if (!file) { 12 | vscode.window.showErrorMessage(noFileSelectedErrorMessage); 13 | return; 14 | } 15 | 16 | // Root SuiteScript folder has to be opened 17 | if (!vscode.workspace.rootPath) { 18 | vscode.window.showErrorMessage(noProjectOpenedErrorMessage); 19 | return; 20 | } 21 | 22 | netSuiteBl.downloadFileFromNetSuite(file); 23 | }); 24 | context.subscriptions.push(downloadFileDisposable); 25 | 26 | let previewFileDisposable = vscode.commands.registerCommand('netsuite-upload.previewFile', (file) => { 27 | if (!file) { 28 | vscode.window.showErrorMessage(noFileSelectedErrorMessage); 29 | return; 30 | } 31 | 32 | // Root SuiteScript folder has to be opened 33 | if (!vscode.workspace.rootPath) { 34 | vscode.window.showErrorMessage(noProjectOpenedErrorMessage); 35 | return; 36 | } 37 | 38 | netSuiteBl.previewFileFromNetSuite(file); 39 | }); 40 | context.subscriptions.push(previewFileDisposable); 41 | 42 | let uploadFileDisposable = vscode.commands.registerCommand('netsuite-upload.uploadFile', (file) => { 43 | // Root SuiteScript folder has to be opened 44 | if (!vscode.workspace.rootPath) { 45 | vscode.window.showErrorMessage(noProjectOpenedErrorMessage); 46 | return; 47 | } 48 | 49 | if (!file || !Object.keys(file).length) { 50 | if (!vscode.window.activeTextEditor && !vscode.window.activeTextEditor.document.uri) { 51 | vscode.window.showErrorMessage(noFileSelectedErrorMessage); 52 | return; 53 | } 54 | else { 55 | file = vscode.window.activeTextEditor.document.uri; 56 | } 57 | } 58 | 59 | netSuiteBl.uploadFileToNetSuite(file); 60 | }); 61 | context.subscriptions.push(uploadFileDisposable); 62 | 63 | let deleteFileDisposable = vscode.commands.registerCommand('netsuite-upload.deleteFile', (file) => { 64 | if (!file) { 65 | vscode.window.showErrorMessage(noFileSelectedErrorMessage); 66 | return; 67 | } 68 | 69 | // Root SuiteScript folder has to be opened 70 | if (!vscode.workspace.rootPath) { 71 | vscode.window.showErrorMessage(noProjectOpenedErrorMessage); 72 | return; 73 | } 74 | 75 | netSuiteBl.deleteFileInNetSuite(file); 76 | }); 77 | context.subscriptions.push(deleteFileDisposable); 78 | 79 | let uploadFolderDisposable = vscode.commands.registerCommand('netsuite-upload.uploadFolder', (directory) => { 80 | // Root SuiteScript folder has to be opened 81 | if (!vscode.workspace.workspaceFolders.length) { 82 | vscode.window.showErrorMessage(noProjectOpenedErrorMessage); 83 | return; 84 | } 85 | 86 | if (!directory || !Object.keys(directory).length) { 87 | if (!vscode.window.activeTextEditor && !vscode.window.activeTextEditor.document.uri) { 88 | vscode.window.showErrorMessage(noFileSelectedErrorMessage); 89 | return; 90 | } 91 | else { 92 | let path = vscode.window.activeTextEditor.document.uri.path; 93 | directory = vscode.Uri.file(path.substring(0, path.lastIndexOf("/"))); 94 | } 95 | } 96 | netSuiteBl.uploadDirectoryToNetSuite(directory); 97 | }); 98 | context.subscriptions.push(uploadFolderDisposable); 99 | 100 | let downloadFolderDisposable = vscode.commands.registerCommand('netsuite-upload.downloadFolder', (directory) => { 101 | if (!directory) { 102 | vscode.window.showErrorMessage('No directory selected.'); 103 | return; 104 | } 105 | 106 | // Root SuiteScript folder has to be opened 107 | if (!vscode.workspace.rootPath) { 108 | vscode.window.showErrorMessage(noProjectOpenedErrorMessage); 109 | return; 110 | } 111 | 112 | netSuiteBl.downloadDirectoryFromNetSuite(directory); 113 | }); 114 | context.subscriptions.push(downloadFolderDisposable); 115 | 116 | let addCustomDependencyDisposable = vscode.commands.registerCommand('netsuite-upload.addCustomDependency', () => { 117 | let editor = vscode.window.activeTextEditor; 118 | if (!editor) { 119 | vscode.window.showErrorMessage('No file is opened.'); 120 | return; 121 | } 122 | 123 | netSuiteBl.addCustomDependencyToActiveFile(editor); 124 | }); 125 | context.subscriptions.push(addCustomDependencyDisposable); 126 | 127 | let addNSDependencyDisposable = vscode.commands.registerCommand('netsuite-upload.addNSDependency', () => { 128 | let editor = vscode.window.activeTextEditor; 129 | if (!editor) { 130 | vscode.window.showErrorMessage('No file is opened.'); 131 | return; 132 | } 133 | 134 | netSuiteBl.addNetSuiteDependencyToActiveFile(editor); 135 | }); 136 | context.subscriptions.push(addNSDependencyDisposable); 137 | 138 | let getRestletVersion = vscode.commands.registerCommand('netsuite-upload.getRestletVersion', () => { 139 | netSuiteBl.getRestletVersion(); 140 | }); 141 | context.subscriptions.push(getRestletVersion); 142 | } 143 | exports.activate = activate; 144 | 145 | // this method is called when your extension is deactivated 146 | function deactivate() { 147 | } 148 | exports.deactivate = deactivate; 149 | -------------------------------------------------------------------------------- /helpers/codeChangeHelper.js: -------------------------------------------------------------------------------- 1 | let fs = require('fs'); 2 | let esprima = require('esprima'); 3 | let estraverse = require('estraverse'); 4 | let vscode = require('vscode'); 5 | 6 | function getCoords(fileContent) { 7 | var tree = esprima.parse(fileContent, { sourceType: 'module', tokens: true, range: true, loc: true }); 8 | 9 | var coords = { 10 | depPath: null, 11 | depParam: null 12 | }; 13 | 14 | estraverse.traverse(tree, { 15 | enter: function (node, parent) { 16 | 17 | if (parent && parent.type == 'CallExpression' && parent.callee.name == 'define') { 18 | if (node.type == 'ArrayExpression') { 19 | coords.depPath = { 20 | start : { 21 | row: node.loc.start.line, 22 | col: node.loc.start.column 23 | }, 24 | end : { 25 | row: node.loc.end.line, 26 | col: node.loc.end.column 27 | }, 28 | range: [node.range[0], node.range[1]] 29 | }; 30 | } 31 | 32 | if (node.type == 'ArrowFunctionExpression' || node.type == 'FunctionExpression') { 33 | coords.depParam = { 34 | start : { 35 | row: node.loc.start.line, 36 | col: node.loc.start.column 37 | }, 38 | end : { 39 | row: node.body.loc.start.line, 40 | col: node.body.loc.start.column 41 | }, 42 | range: [node.range[0], node.body.range[0]] 43 | }; 44 | } 45 | } 46 | } 47 | }); 48 | 49 | return coords; 50 | } 51 | 52 | function createPosition(row, col) { 53 | return new vscode.Position(row, col); 54 | } 55 | 56 | function rangeFactory(start, end) { 57 | return new vscode.Range(start, end); 58 | } 59 | 60 | function textEditFactory(range, content) { 61 | return new vscode.TextEdit(range, content); 62 | } 63 | 64 | function editFactory (coords, content){ 65 | var start = createPosition(coords.start.line, coords.start.char); 66 | var end = createPosition(coords.end.line, coords.end.char); 67 | var range = rangeFactory(start, end); 68 | 69 | return textEditFactory(range, content); 70 | } 71 | 72 | function workspaceEditFactory() { 73 | return new vscode.WorkspaceEdit(); 74 | } 75 | 76 | function setEditFactory(uri, coords, content) { 77 | var workspaceEdit = workspaceEditFactory(); 78 | var edit = editFactory(coords, content); 79 | 80 | workspaceEdit.set(uri, [edit]); 81 | return workspaceEdit; 82 | } 83 | 84 | function getDocument (vsEditor) { 85 | return typeof vsEditor._documentData !== 'undefined' ? vsEditor._documentData : vsEditor._document 86 | } 87 | 88 | async function editCurrentDocument(vsEditor, coords, content){ 89 | var vsDocument = getDocument(vsEditor); 90 | var edit = setEditFactory(vsDocument._uri, coords, content); 91 | await vscode.workspace.applyEdit(edit); 92 | } 93 | 94 | function replaceStringRange(source, replacement, from, to) { 95 | return source.substring(0, from) + replacement + source.substring(to); 96 | } 97 | 98 | function getUpdatedFunctionParams(newDependecyParam, oldString) { 99 | let newString = oldString; 100 | 101 | if (oldString.indexOf('function') != -1) { 102 | let fromIndex = oldString.indexOf('(') + 1; 103 | let toIndex = oldString.indexOf(')'); 104 | let oldParams = oldString.substring(fromIndex, toIndex); 105 | let newParams = oldParams.trim(); 106 | newParams = newParams.length > 0 ? newParams + ', ' + newDependecyParam : newDependecyParam; 107 | 108 | newString = replaceStringRange(oldString, newParams, fromIndex, toIndex); 109 | } 110 | if (oldString.indexOf('=>') != -1) { 111 | let params = oldString.split("=>")[0].trim(); 112 | 113 | if (params.indexOf('(') == -1) { // "param => " 114 | newString = '(' + params + ', ' + newDependecyParam + ') => '; 115 | } else { 116 | let fromIndex = oldString.indexOf('(') + 1; 117 | let toIndex = oldString.indexOf(')'); 118 | let oldParams = oldString.substring(fromIndex, toIndex); 119 | let newParams = oldParams.trim(); 120 | newParams = newParams.length > 0 ? newParams + ', ' + newDependecyParam : newDependecyParam; 121 | 122 | newString = replaceStringRange(oldString, newParams, fromIndex, toIndex); 123 | } 124 | } 125 | 126 | return newString; 127 | } 128 | 129 | function getUpdatedDepPath(newDependecyPath, oldString) { 130 | if (oldString) { 131 | let fromIndex = oldString.indexOf('[') + 1; 132 | let toIndex = oldString.indexOf(']'); 133 | let oldPaths = oldString.substring(fromIndex, toIndex); 134 | let newPaths = oldPaths.trim(); 135 | newPaths = newPaths.length > 0 ? newPaths + ", '" + newDependecyPath + "'" : "'" + newDependecyPath + "'"; 136 | 137 | return replaceStringRange(oldString, newPaths, fromIndex, toIndex); 138 | } 139 | 140 | return "['" + newDependecyPath + "']"; 141 | } 142 | 143 | async function updateDocument(editor, startLine, startChar, endLine, endChar, content) { 144 | var editorCoords = { 145 | start : { 146 | line: startLine, 147 | char: startChar 148 | }, 149 | end : { 150 | line: endLine, 151 | char: endChar 152 | } 153 | } 154 | 155 | await editCurrentDocument(editor, editorCoords, content); 156 | } 157 | 158 | exports.getCoords = getCoords; 159 | exports.getUpdatedFunctionParams = getUpdatedFunctionParams; 160 | exports.getUpdatedDepPath = getUpdatedDepPath; 161 | exports.createPosition = createPosition; 162 | exports.editCurrentDocument = editCurrentDocument; 163 | exports.updateDocument = updateDocument; 164 | -------------------------------------------------------------------------------- /helpers/netSuiteRestClient.js: -------------------------------------------------------------------------------- 1 | const vscode = require('vscode'); 2 | const superagent = require('superagent'); 3 | const crypto = require('crypto'); 4 | const path = require('path'); 5 | const OAuth = require('oauth-1.0a'); 6 | const compareVersions = require('compare-versions'); 7 | const url = require('url'); 8 | const BAD_VERSION_ERROR = { 9 | shortmessage: "You might need to update the vscodeExtensionRestlet.js RESTlet in NetSuite to the latest version." 10 | }; 11 | 12 | function getRelativePath(absFilePath) { 13 | var rootDirectory = vscode.workspace.getConfiguration('netSuiteUpload').rootDirectory; 14 | if (rootDirectory) { 15 | return path.join(rootDirectory, absFilePath.slice(vscode.workspace.rootPath.length)); 16 | } else { 17 | return path.join('SuiteScripts', absFilePath.slice(vscode.workspace.rootPath.length)); 18 | } 19 | } 20 | 21 | function getFile(file, callback) { 22 | getData('file', file.fsPath, callback); 23 | } 24 | 25 | function getDirectory(directory, callback) { 26 | getData('directory', directory.fsPath, callback); 27 | } 28 | 29 | function getAuthHeader(method, data) { 30 | var nlAuth = vscode.workspace.getConfiguration('netSuiteUpload').authentication; 31 | var netSuiteOAuthKey = vscode.workspace.getConfiguration('netSuiteUpload').netSuiteKey; 32 | 33 | if (nlAuth && nlAuth.length > 0) { 34 | return vscode.workspace.getConfiguration('netSuiteUpload').authentication; 35 | } 36 | if (netSuiteOAuthKey && netSuiteOAuthKey.length > 0) { 37 | const opts = { 38 | consumer: { 39 | key: vscode.workspace.getConfiguration('netSuiteUpload').consumerToken, 40 | secret: vscode.workspace.getConfiguration('netSuiteUpload').consumerSecret 41 | }, 42 | signature_method: 'HMAC-SHA256', 43 | realm: vscode.workspace.getConfiguration('netSuiteUpload').realm, 44 | hash_function: function (base_string, key) { 45 | return crypto.createHmac('sha256', key).update(base_string).digest('base64'); 46 | } 47 | }; 48 | 49 | const oauth = OAuth(opts); 50 | 51 | var token = { 52 | key: vscode.workspace.getConfiguration('netSuiteUpload').netSuiteKey, 53 | secret: vscode.workspace.getConfiguration('netSuiteUpload').netSuiteSecret 54 | }; 55 | var restletUrl = vscode.workspace.getConfiguration('netSuiteUpload').restlet; 56 | var url_parts = url.parse(restletUrl, true); 57 | 58 | // Build up the data payload to sign. 59 | // qs will contain the script and deploy params. 60 | var qs = url_parts.query; 61 | var mergedData; 62 | if (method === 'GET' || method === 'DELETE') { 63 | // For GETs and DELETES, data ends up in the querystring. 64 | Object.assign(qs, data); 65 | mergedData = qs; 66 | } else { 67 | // for POSTs and DELETEs, the data isn't in the querystring 68 | // so we don't need it in the oauth signature. 69 | mergedData = qs; 70 | } 71 | var header = oauth.toHeader(oauth.authorize({ 72 | method: method, 73 | url: restletUrl, 74 | data: mergedData 75 | }, token)); 76 | 77 | console.log(header.Authorization); 78 | return header.Authorization; 79 | } 80 | 81 | throw "No authentication method found in settings.json (user or workspace settings)."; 82 | } 83 | 84 | function doesRestletNeedUpdating(needsUpdating) { 85 | getRestletVersion((err, res) => { 86 | if (err || (compareVersions(res.body.restletVersion, "1.0.2") === -1)) { 87 | needsUpdating(true, err); 88 | } else { 89 | needsUpdating(false, err); 90 | } 91 | }); 92 | } 93 | 94 | function getData(type, objectPath, callback) { 95 | doesRestletNeedUpdating(function (needsUpdating, err) { 96 | if (needsUpdating) { 97 | callback(BAD_VERSION_ERROR, err); 98 | return; 99 | } 100 | 101 | var relativeName = getRelativePath(objectPath); 102 | var data = { 103 | type: type, 104 | name: relativeName 105 | }; 106 | superagent.get(vscode.workspace.getConfiguration('netSuiteUpload').restlet) 107 | .set("Content-Type", "application/json") 108 | .set("Authorization", getAuthHeader('GET', data)) 109 | .query(data) 110 | .end((err, res) => { 111 | callback(err, res); 112 | }); 113 | }); 114 | } 115 | 116 | function getRestletVersion(callback) { 117 | var data = { 118 | type: "version" 119 | }; 120 | superagent.get(vscode.workspace.getConfiguration('netSuiteUpload').restlet) 121 | .set("Content-Type", "application/json") 122 | .set("Authorization", getAuthHeader('GET', data)) 123 | .query(data) 124 | .end((err, res) => { 125 | callback(err, res); 126 | }); 127 | } 128 | 129 | function postFile(file, content, callback) { 130 | postData('file', file.fsPath, content, callback); 131 | 132 | } 133 | 134 | function postData(type, objectPath, content, callback) { 135 | doesRestletNeedUpdating(function (needsUpdating, err) { 136 | if (needsUpdating) { 137 | callback(BAD_VERSION_ERROR, err); 138 | return; 139 | } 140 | 141 | var relativeName = getRelativePath(objectPath); 142 | var data = { 143 | type: type, 144 | name: relativeName, 145 | content: content 146 | }; 147 | superagent.post(vscode.workspace.getConfiguration('netSuiteUpload').restlet) 148 | .set("Content-Type", "application/json") 149 | .set("Authorization", getAuthHeader('POST', data)) 150 | .send(data) 151 | .end((err, res) => { 152 | callback(err, res); 153 | }); 154 | }); 155 | } 156 | 157 | function deleteFile(file, callback) { 158 | if (doesRestletNeedUpdating(callback)) return; 159 | deleteData('file', file.fsPath, callback); 160 | } 161 | 162 | function deleteData(type, objectPath, callback) { 163 | 164 | doesRestletNeedUpdating(function (needsUpdating, err) { 165 | if (needsUpdating) { 166 | callback(BAD_VERSION_ERROR, err); 167 | return; 168 | } 169 | var relativeName = getRelativePath(objectPath); 170 | var data = { 171 | type: type, 172 | name: relativeName 173 | }; 174 | superagent.delete(vscode.workspace.getConfiguration('netSuiteUpload').restlet) 175 | .set("Content-Type", "application/json") 176 | .set("Authorization", getAuthHeader('DELETE', data)) 177 | .query(data) 178 | .end((err, res) => { 179 | callback(err, res); 180 | }); 181 | }); 182 | } 183 | 184 | exports.getRelativePath = getRelativePath; 185 | exports.getFile = getFile; 186 | exports.postFile = postFile; 187 | exports.deleteFile = deleteFile; 188 | exports.getDirectory = getDirectory; 189 | exports.getRestletVersion = getRestletVersion; -------------------------------------------------------------------------------- /helpers/netsuiteList.js: -------------------------------------------------------------------------------- 1 | function getSuiteScriptDependecies() { 2 | return [ 3 | { path: 'N/auth', param: 'auth' }, 4 | { path: 'N/cache', param: 'cache' }, 5 | { path: 'N/config', param: 'config' }, 6 | { path: 'N/crypto', param: 'crypto' }, 7 | { path: 'N/currency', param: 'currency' }, 8 | { path: 'N/currentRecord', param: 'currentRecord' }, 9 | { path: 'N/email', param: 'email' }, 10 | { path: 'N/encode', param: 'encode' }, 11 | { path: 'N/error', param: 'error' }, 12 | { path: 'N/file', param: 'file' }, 13 | { path: 'N/format', param: 'format' }, 14 | { path: 'N/http', param: 'http' }, 15 | { path: 'N/https', param: 'https' }, 16 | { path: 'N/log', param: 'log' }, 17 | { path: 'N/plugin', param: 'plugin' }, 18 | { path: 'N/portlet', param: 'portlet' }, 19 | { path: 'N/record', param: 'record' }, 20 | { path: 'N/redirect', param: 'redirect' }, 21 | { path: 'N/render', param: 'render' }, 22 | { path: 'N/runtime', param: 'runtime' }, 23 | { path: 'N/search', param: 'search' }, 24 | { path: 'N/sftp', param: 'sftp' }, 25 | { path: 'N/sso', param: 'sso' }, 26 | { path: 'N/task', param: 'task' }, 27 | { path: 'N/transaction', param: 'transaction' }, 28 | { path: 'N/ui/dialog', param: 'dialog' }, 29 | { path: 'N/ui/message', param: 'message' }, 30 | { path: 'N/ui/serverWidget', param: 'serverWidget' }, 31 | { path: 'N/url', param: 'url' }, 32 | { path: 'N/util', param: 'util' }, 33 | { path: 'N/workflow', param: 'workflow' }, 34 | { path: 'N/xml', param: 'xml' } 35 | ]; 36 | } 37 | 38 | exports.getSuiteScriptDependecies = getSuiteScriptDependecies; 39 | -------------------------------------------------------------------------------- /helpers/uiHelper.js: -------------------------------------------------------------------------------- 1 | let vscode = require('vscode'); 2 | 3 | function askForCustomDependency() { 4 | var depPath; 5 | 6 | return vscode.window.showInputBox({prompt: 'Please type the dependency path'}) 7 | .then(path => { 8 | depPath = path; 9 | 10 | return vscode.window.showInputBox({prompt: 'Please type the dependecy parameter name'}) 11 | .then(param => { 12 | return { 13 | depPath: depPath, 14 | depParam: param 15 | } 16 | }) 17 | }) 18 | } 19 | 20 | function showListOfNetSuiteDependecies(list) { 21 | return vscode.window.showQuickPick(list); 22 | } 23 | 24 | exports.askForCustomDependency = askForCustomDependency; 25 | exports.showListOfNetSuiteDependecies = showListOfNetSuiteDependecies; 26 | -------------------------------------------------------------------------------- /img/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netsuite-upload-org/netsuite-upload/da474286811fec6c841a9c66a0987ec02565c52c/img/icon.png -------------------------------------------------------------------------------- /img/netsuite_upload.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netsuite-upload-org/netsuite-upload/da474286811fec6c841a9c66a0987ec02565c52c/img/netsuite_upload.gif -------------------------------------------------------------------------------- /img/snippet_addModule.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netsuite-upload-org/netsuite-upload/da474286811fec6c841a9c66a0987ec02565c52c/img/snippet_addModule.gif -------------------------------------------------------------------------------- /netSuiteRestlet/vscodeExtensionRestlet.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @NApiVersion 2.x 3 | * @NScriptType Restlet 4 | */ 5 | 6 | define(['N/file', 'N/search', 'N/record', 'N/log', 'N/error'], function ( 7 | file, 8 | search, 9 | record, 10 | log, 11 | error 12 | ) { 13 | 14 | const RESTLET_VERSION = '1.0.2'; 15 | const RESTLET_NAME = 'vscodeExtensionRestlet.js'; 16 | 17 | 18 | var fileMap = {}; 19 | fileMap.appcache = file.Type.APPCACHE; 20 | fileMap.dwf = file.Type.AUTOCAD; 21 | fileMap.dwg = file.Type.AUTOCAD; 22 | fileMap.dxf = file.Type.AUTOCAD; 23 | fileMap.dwf = file.Type.AUTOCAD; 24 | fileMap.dwt = file.Type.AUTOCAD; 25 | fileMap.plt = file.Type.AUTOCAD; 26 | fileMap.bmp = file.Type.BMPIMAGE; 27 | fileMap.cer = file.Type.CERTIFICATE; 28 | fileMap.crl = file.Type.CERTIFICATE; 29 | fileMap.crt = file.Type.CERTIFICATE; 30 | fileMap.csr = file.Type.CERTIFICATE; 31 | fileMap.der = file.Type.CERTIFICATE; 32 | fileMap.key = file.Type.CERTIFICATE; 33 | fileMap.p10 = file.Type.CERTIFICATE; 34 | fileMap.p12 = file.Type.CERTIFICATE; 35 | fileMap.p7b = file.Type.CERTIFICATE; 36 | fileMap.p7c = file.Type.CERTIFICATE; 37 | fileMap.p7r = file.Type.CERTIFICATE; 38 | fileMap.p8 = file.Type.CERTIFICATE; 39 | fileMap.pem = file.Type.CERTIFICATE; 40 | fileMap.pfx = file.Type.CERTIFICATE; 41 | fileMap.spc = file.Type.CERTIFICATE; 42 | fileMap.config = file.Type.CONFIG; 43 | fileMap.csv = file.Type.CSV; 44 | fileMap.xls = file.Type.EXCEL; 45 | fileMap.xlsx = file.Type.EXCEL; 46 | fileMap.swf = file.Type.FLASH; 47 | fileMap.ftl = file.Type.FREEMARKER; 48 | fileMap.gif = file.Type.GIFIMAGE; 49 | fileMap.gz = file.Type.GZIP; 50 | fileMap.htm = file.Type.HTMLDOC; 51 | fileMap.html = file.Type.HTMLDOC; 52 | fileMap.shtml = file.Type.HTMLDOC; 53 | fileMap.ico = file.Type.ICON; 54 | fileMap.icon = file.Type.ICON; 55 | fileMap.js = file.Type.JAVASCRIPT; 56 | fileMap.jpg = file.Type.JPGIMAGE; 57 | fileMap.jpeg = file.Type.JPGIMAGE; 58 | fileMap.json = file.Type.JSON; 59 | fileMap.eml = file.Type.MESSAGERFC; 60 | fileMap.mp3 = file.Type.MP3; 61 | fileMap.mpg = fileMap.mpeg = file.Type.MPEGMOVIE; 62 | fileMap.mpp = fileMap.mpt = file.Type.MSPROJECT; 63 | fileMap.pdf = file.Type.PDF; 64 | fileMap.pjpeg = file.Type.PJPGIMAGE; 65 | fileMap.prn = fileMap.txt = fileMap.log = fileMap.htc = fileMap.sql = fileMap.ts = file.Type.PLAINTEXT; 66 | fileMap.png = file.Type.PNGIMAGE; 67 | fileMap.ps = fileMap.eps = file.Type.POSTSCRIPT; 68 | fileMap.ppt = fileMap.pptx = file.Type.POWERPOINT; 69 | fileMap.qt = fileMap.mov = file.Type.QUICKTIME; 70 | fileMap.rtf = file.Type.RTF; 71 | fileMap.scss = file.Type.SCSS; 72 | fileMap.sms = file.Type.SMS; 73 | fileMap.css = file.Type.STYLESHEET; 74 | fileMap.svg = file.Type.SVG; 75 | fileMap.tar = fileMap.tgz = fileMap.tbz = file.Type.TAR; 76 | fileMap.tif = fileMap.tiff = file.Type.TIFFIMAGE; 77 | fileMap.vsd = fileMap.vsdx = file.Type.VISIO; 78 | fileMap.ssp = file.Type.WEBAPPPAGE; 79 | fileMap.ss = file.Type.WEBAPPSCRIPT; 80 | fileMap.doc = fileMap.docx = fileMap.dot = file.Type.WORD; 81 | fileMap.xml = file.Type.XMLDOC; 82 | fileMap.xsd = file.Type.XSD; 83 | fileMap.zip = fileMap.lzh = fileMap.lha = file.Type.ZIP; 84 | 85 | function throwError(message) { 86 | var errorObj = error.create({ 87 | name: RESTLET_NAME, 88 | message: message, 89 | notifyOff: false 90 | }); 91 | log.error(errorObj); 92 | throw (errorObj); 93 | } 94 | 95 | function getFolderId(folderPath) { 96 | var foldersArray = folderPath.split('/'); 97 | var folderName = foldersArray[foldersArray.length - 1]; 98 | var filters = []; 99 | 100 | filters.push({ 101 | name: 'name', 102 | operator: 'is', 103 | values: [folderName] 104 | }); 105 | if (foldersArray.length == 1) 106 | filters.push({ 107 | name: 'istoplevel', 108 | operator: 'is', 109 | values: true 110 | }); 111 | 112 | if (foldersArray.length > 1) { 113 | var parentFolderArray = foldersArray.slice(0, -1); 114 | var parentId = getFolderId(parentFolderArray.join('/')); 115 | filters.push({ 116 | name: 'parent', 117 | operator: 'anyof', 118 | values: [parentId] 119 | }); 120 | } 121 | 122 | var folderSearch = search.create({ 123 | type: search.Type.FOLDER, 124 | filters: filters 125 | }); 126 | 127 | var folderId = null; 128 | folderSearch.run().each(function (result) { 129 | folderId = result.id; 130 | return false; 131 | }); 132 | 133 | return folderId; 134 | } 135 | 136 | function createFolderIfNotExist(folderPath, parentId) { 137 | var folderArray = folderPath.split('/'); 138 | var firstFolder = folderArray[0]; 139 | var nextFolders = folderArray.slice(1); 140 | var filters = []; 141 | 142 | filters.push({ 143 | name: 'name', 144 | operator: 'is', 145 | values: [firstFolder] 146 | }); 147 | if (parentId) { 148 | filters.push({ 149 | name: 'parent', 150 | operator: 'anyof', 151 | values: [parentId] 152 | }); 153 | } else { 154 | filters.push({ 155 | name: 'istoplevel', 156 | operator: 'is', 157 | values: true 158 | }); 159 | } 160 | 161 | var folderSearch = search.create({ 162 | type: search.Type.FOLDER, 163 | filters: filters 164 | }); 165 | 166 | var folderId = null; 167 | folderSearch.run().each(function (result) { 168 | folderId = result.id; 169 | return false; 170 | }); 171 | 172 | if (!folderId) { 173 | var folderRecord = record.create({ 174 | type: record.Type.FOLDER 175 | }); 176 | folderRecord.setValue({ 177 | fieldId: 'name', 178 | value: firstFolder 179 | }); 180 | folderRecord.setValue({ 181 | fieldId: 'parent', 182 | value: parentId 183 | }); 184 | folderId = folderRecord.save(); 185 | } 186 | 187 | if (!nextFolders || nextFolders.length == 0) return folderId; 188 | 189 | return createFolderIfNotExist(nextFolders.join('/'), folderId); 190 | } 191 | 192 | function getInnerFolders(folderPath, folderId) { 193 | var folderSearch = search.create({ 194 | type: search.Type.FOLDER, 195 | columns: ['name'], 196 | filters: [{ 197 | name: 'parent', 198 | operator: 'is', 199 | values: [folderId] 200 | }] 201 | }); 202 | 203 | var innerFolders = [{ 204 | id: folderId, 205 | path: folderPath 206 | }]; 207 | folderSearch.run().each(function (result) { 208 | innerFolders = innerFolders.concat( 209 | getInnerFolders(folderPath + '/' + result.getValue('name'), result.id) 210 | ); 211 | return true; 212 | }); 213 | return innerFolders; 214 | } 215 | 216 | function getFilesInFolder(folderPath, folderId) { 217 | var fileSearch = search.create({ 218 | type: search.Type.FOLDER, 219 | columns: ['file.internalid', 'file.name'], 220 | filters: [{ 221 | name: 'internalid', 222 | operator: 'is', 223 | values: [folderId] 224 | }] 225 | }); 226 | 227 | var files = []; 228 | fileSearch.run().each(function (result) { 229 | var fileId = result.getValue({ 230 | name: 'internalid', 231 | join: 'file' 232 | }); 233 | if (fileId) { 234 | var fileName = result.getValue({ 235 | name: 'name', 236 | join: 'file' 237 | }); 238 | var fileContent = file 239 | .load({ 240 | id: fileId 241 | }) 242 | .getContents(); 243 | 244 | files.push({ 245 | type: 'file', 246 | name: fileName, 247 | fullPath: folderPath + '/' + fileName, 248 | content: fileContent 249 | }); 250 | } 251 | return true; 252 | }); 253 | 254 | // In case of empty folder return the folder name 255 | if (files.length == 0) { 256 | files.push({ 257 | type: 'folder', 258 | fullPath: folderPath 259 | }); 260 | } 261 | 262 | return files; 263 | } 264 | 265 | function getFile(relFilePath) { 266 | var fullFilePath = relFilePath; 267 | 268 | var fileToReturn = file.load({ 269 | id: fullFilePath 270 | }); 271 | 272 | return [{ 273 | name: fileToReturn.name, 274 | fullPath: fullFilePath, 275 | content: fileToReturn.getContents() 276 | }]; 277 | } 278 | 279 | function getDirectory(relDirectoryPath) { 280 | var folderId = getFolderId(relDirectoryPath); 281 | var folders = getInnerFolders(relDirectoryPath, folderId); 282 | var allFiles = []; 283 | 284 | folders.forEach(function (folder) { 285 | allFiles = allFiles.concat(getFilesInFolder(folder.path, folder.id)); 286 | }); 287 | return allFiles; 288 | } 289 | 290 | function updateFile(existingFile, content) { 291 | var fileObj = file.create({ 292 | name: existingFile.name, 293 | fileType: existingFile.fileType, 294 | contents: content, 295 | description: existingFile.description, 296 | encoding: existingFile.encoding, 297 | folder: existingFile.folder, 298 | isOnline: existingFile.isOnline 299 | }); 300 | fileObj.save(); 301 | } 302 | 303 | function createFile(filePath, content) { 304 | var pathArray = filePath.split('/'); 305 | var name = pathArray[pathArray.length - 1]; 306 | var fileType = getFileType(name); 307 | var folder = createFolderIfNotExist( 308 | filePath.substring(0, filePath.lastIndexOf('/')) 309 | ); 310 | 311 | var fileObj = file.create({ 312 | name: name, 313 | fileType: fileType, 314 | contents: content, 315 | folder: folder 316 | }); 317 | fileObj.save(); 318 | } 319 | 320 | function getFileType(fileName) { 321 | var extension = fileName 322 | .split('.') 323 | .pop() 324 | .toLowerCase(); 325 | 326 | var fileType = fileMap[extension]; 327 | if (fileType === null) 328 | return "UNKNOWN"; 329 | return fileType; 330 | } 331 | 332 | function postFile(relFilePath, content) { 333 | var fullFilePath = relFilePath; 334 | 335 | try { 336 | var loadedFile = file.load({ 337 | id: fullFilePath 338 | }); 339 | updateFile(loadedFile, content); 340 | } catch (e) { 341 | if (e.name == 'RCRD_DSNT_EXIST') { 342 | createFile(fullFilePath, content); 343 | } else { 344 | throw e; 345 | } 346 | } 347 | } 348 | 349 | function deleteFile(relFilePath) { 350 | var fullFilePath = relFilePath; 351 | 352 | var fileObject = file.load({ 353 | id: fullFilePath 354 | }); 355 | file.delete({ 356 | id: fileObject.id 357 | }); 358 | } 359 | 360 | function getFunc(request) { 361 | var type = request.type; 362 | if (type == null || type.length === 0) { 363 | throwError("type parameter must be one of 'file', 'directory' or 'version'"); 364 | return; 365 | } 366 | 367 | if (type === 'version') { 368 | return { 369 | restletVersion: RESTLET_VERSION 370 | }; 371 | } 372 | 373 | var name = request.name; 374 | if (name == null || name.length === 0) { 375 | throwError("'name' parameter must be set when type is 'file' or 'directory'. It is the file or directory name."); 376 | return; 377 | } 378 | 379 | var relPath = request.name.replace(/\\/g, '/'); 380 | 381 | if (type === 'file') { 382 | return getFile(relPath); 383 | } 384 | if (type === 'directory') { 385 | return getDirectory(relPath); 386 | } 387 | } 388 | 389 | function postFunc(request) { 390 | var relPath = request.name.replace(/\\/g, '/'); 391 | postFile(relPath, request.content); 392 | return true; 393 | } 394 | 395 | function deleteFunc(request) { 396 | var relPath = request.name.replace(/\\/g, '/'); 397 | deleteFile(relPath); 398 | return true; 399 | } 400 | 401 | return { 402 | get: getFunc, 403 | post: postFunc, 404 | delete: deleteFunc 405 | }; 406 | }); 407 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "netsuite-upload", 3 | "displayName": "NetSuite Upload", 4 | "description": "Upload and download NetSuite SuiteScript files and folders.", 5 | "version": "1.2.4", 6 | "publisher": "nsupload-org", 7 | "engines": { 8 | "vscode": "^1.22.2" 9 | }, 10 | "categories": [ 11 | "Other" 12 | ], 13 | "homepage": "https://github.com/netsuite-upload-org/netsuite-upload", 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/netsuite-upload-org/netsuite-upload" 17 | }, 18 | "icon": "img/icon.png", 19 | "keywords": [ 20 | "netsuite", 21 | "netsuite upload", 22 | "netsuite download", 23 | "netsuite sync" 24 | ], 25 | "activationEvents": [ 26 | "onCommand:netsuite-upload.downloadFile", 27 | "onCommand:netsuite-upload.uploadFile", 28 | "onCommand:netsuite-upload.deleteFile", 29 | "onCommand:netsuite-upload.previewFile", 30 | "onCommand:netsuite-upload.uploadFolder", 31 | "onCommand:netsuite-upload.downloadFolder", 32 | "onCommand:netsuite-upload.addCustomDependency", 33 | "onCommand:netsuite-upload.addNSDependency", 34 | "onCommand:netsuite-upload.getRestletVersion" 35 | ], 36 | "main": "./extension", 37 | "contributes": { 38 | "configuration": { 39 | "type": "object", 40 | "title": "NetSuite Connection Configuration", 41 | "properties": { 42 | "netSuiteUpload.authentication": { 43 | "type": "string", 44 | "description": "NLAuth-style authentication header. To use OAuth instead, leave this unset." 45 | }, 46 | "netSuiteUpload.restlet": { 47 | "type": "string", 48 | "description": "Restlet URL. After you deploy the vscodeExtensionRestlet.js, set this to the Script Deployment External URL." 49 | }, 50 | "netSuiteUpload.tempFolder": { 51 | "type": "string", 52 | "description": "Temporary folder (e.g. C:\\temp or /tmp). Used during diff comparisons between local and remote files." 53 | }, 54 | "netSuiteUpload.netSuiteKey": { 55 | "type": "string", 56 | "description": "OAuth NetSuite Token ID" 57 | }, 58 | "netSuiteUpload.netSuiteSecret": { 59 | "type": "string", 60 | "description": "OAuth NetSuite Token Secret" 61 | }, 62 | "netSuiteUpload.consumerToken": { 63 | "type": "string", 64 | "description": "OAuth NetSuite Consumer Key" 65 | }, 66 | "netSuiteUpload.consumerSecret": { 67 | "type": "string", 68 | "description": "OAuth NetSuite Consumer Secret" 69 | }, 70 | "netSuiteUpload.realm": { 71 | "type": "string", 72 | "description": "Your NetSuite numeric account number (used for OAuth 'realm' parameter)" 73 | }, 74 | "netSuiteUpload.rootDirectory": { 75 | "type": "string", 76 | "description": "NetSuite File Cabinet SuiteScripts sub-folder path to upload to. Defaults to 'SuiteScripts' if not set." 77 | } 78 | } 79 | }, 80 | "commands": [ 81 | { 82 | "command": "netsuite-upload.downloadFile", 83 | "title": "Pull file from NetSuite", 84 | "category": "NetSuite" 85 | }, 86 | { 87 | "command": "netsuite-upload.previewFile", 88 | "title": "Compare file with NetSuite", 89 | "category": "NetSuite" 90 | }, 91 | { 92 | "command": "netsuite-upload.uploadFile", 93 | "title": "Push file to NetSuite", 94 | "category": "NetSuite" 95 | }, 96 | { 97 | "command": "netsuite-upload.deleteFile", 98 | "title": "Delete file in NetSuite", 99 | "category": "NetSuite" 100 | }, 101 | { 102 | "command": "netsuite-upload.uploadFolder", 103 | "title": "Push folder to NetSuite", 104 | "category": "NetSuite" 105 | }, 106 | { 107 | "command": "netsuite-upload.downloadFolder", 108 | "title": "Pull folder from NetSuite", 109 | "category": "NetSuite" 110 | }, 111 | { 112 | "command": "netsuite-upload.addCustomDependency", 113 | "title": "Add Custom Dependency", 114 | "category": "NetSuite" 115 | }, 116 | { 117 | "command": "netsuite-upload.addNSDependency", 118 | "title": "Add NetSuite Dependency", 119 | "category": "NetSuite" 120 | }, 121 | { 122 | "command": "netsuite-upload.getRestletVersion", 123 | "title": "Get RESTlet version", 124 | "category": "NetSuite" 125 | } 126 | ], 127 | "menus": { 128 | "editor/context": [ 129 | { 130 | "command": "netsuite-upload.downloadFile", 131 | "group": "netsuite" 132 | }, 133 | { 134 | "command": "netsuite-upload.uploadFile", 135 | "group": "netsuite" 136 | }, 137 | { 138 | "command": "netsuite-upload.previewFile", 139 | "group": "netsuite" 140 | } 141 | ], 142 | "explorer/context": [ 143 | { 144 | "when": "!explorerResourceIsFolder", 145 | "command": "netsuite-upload.downloadFile", 146 | "group": "netsuite" 147 | }, 148 | { 149 | "when": "!explorerResourceIsFolder", 150 | "command": "netsuite-upload.previewFile", 151 | "group": "netsuite" 152 | }, 153 | { 154 | "when": "!explorerResourceIsFolder", 155 | "command": "netsuite-upload.uploadFile", 156 | "group": "netsuite" 157 | }, 158 | { 159 | "when": "!explorerResourceIsFolder", 160 | "command": "netsuite-upload.deleteFile", 161 | "group": "netsuite" 162 | }, 163 | { 164 | "when": "explorerResourceIsFolder", 165 | "command": "netsuite-upload.uploadFolder", 166 | "group": "netsuite" 167 | }, 168 | { 169 | "when": "explorerResourceIsFolder", 170 | "command": "netsuite-upload.downloadFolder", 171 | "group": "netsuite" 172 | } 173 | ] 174 | }, 175 | "keybindings": [ 176 | { 177 | "command": "netsuite-upload.downloadFile", 178 | "key": "ctrl+n ctrl+d" 179 | }, 180 | { 181 | "command": "netsuite-upload.uploadFile", 182 | "key": "ctrl+n ctrl+u" 183 | } 184 | ], 185 | "snippets": [ 186 | { 187 | "language": "javascript", 188 | "path": "./snippets/snippets.json" 189 | } 190 | ] 191 | }, 192 | "scripts": { 193 | "postinstall": "node ./node_modules/vscode/bin/install" 194 | }, 195 | "devDependencies": { 196 | "@types/mocha": "^5.2.7", 197 | "@types/node": "^12.6.8", 198 | "eslint": "^6.1.0", 199 | "mocha": "^6.2.0", 200 | "typescript": "^3.5.3", 201 | "vscode": "^1.1.35", 202 | "webpack": "^4.38.0", 203 | "webpack-cli": "^3.3.6" 204 | }, 205 | "dependencies": { 206 | "compare-versions": "^3.5.0", 207 | "crypto": "1.0.1", 208 | "esprima": "4.0.1", 209 | "estraverse": "4.2.0", 210 | "oauth-1.0a": "^2.2.6", 211 | "superagent": "^5.1.0", 212 | "underscore": "1.9.1" 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /snippets/snippets.json: -------------------------------------------------------------------------------- 1 | { 2 | "defineCustomModule" : { 3 | "prefix": "defineModule", 4 | "description": "Define custom SuiteScript AMD module", 5 | "body": [ 6 | "define([], function() {", 7 | "", 8 | " return {", 9 | " $0", 10 | " }", 11 | "});", 12 | "" 13 | ] 14 | }, 15 | "requireCustomModule" : { 16 | "prefix": "requireModule", 17 | "description": "Require custom SuiteScript AMD module", 18 | "body": [ 19 | "require([], function() {", 20 | "", 21 | " $0", 22 | "", 23 | "});", 24 | "" 25 | ] 26 | }, 27 | "defineRestlet" : { 28 | "prefix": "defineRestlet", 29 | "description": "Define Restlet script module", 30 | "body": [ 31 | "/**", 32 | " *@NApiVersion 2.x", 33 | " *@NScriptType Restlet", 34 | " */", 35 | "define([], function() {", 36 | "", 37 | " function _get(context) {", 38 | " $0", 39 | " }", 40 | "", 41 | " function _post(context) {", 42 | " ", 43 | " }", 44 | "", 45 | " function _put(context) {", 46 | " ", 47 | " }", 48 | "", 49 | " function _delete(context) {", 50 | " ", 51 | " }", 52 | "", 53 | " return {", 54 | " get: _get,", 55 | " post: _post,", 56 | " put: _put,", 57 | " delete: _delete", 58 | " }", 59 | "});", 60 | "" 61 | ] 62 | }, 63 | "defineUserEvent" : { 64 | "prefix": "defineUserEvent", 65 | "description": "Define User Event script module", 66 | "body": [ 67 | "/**", 68 | " *@NApiVersion 2.x", 69 | " *@NScriptType UserEventScript", 70 | " */", 71 | "define([], function() {", 72 | "", 73 | " function beforeLoad(context) {", 74 | " $0", 75 | " }", 76 | "", 77 | " function beforeSubmit(context) {", 78 | " ", 79 | " }", 80 | "", 81 | " function afterSubmit(context) {", 82 | " ", 83 | " }", 84 | "", 85 | " return {", 86 | " beforeLoad: beforeLoad,", 87 | " beforeSubmit: beforeSubmit,", 88 | " afterSubmit: afterSubmit", 89 | " }", 90 | "});", 91 | "" 92 | ] 93 | }, 94 | "defineClient" : { 95 | "prefix": "defineClient", 96 | "description": "Define Client script module", 97 | "body": [ 98 | "/**", 99 | " *@NApiVersion 2.x", 100 | " *@NScriptType ClientScript", 101 | " */", 102 | "define([], function() {", 103 | "", 104 | " function pageInit(context) {", 105 | " $0", 106 | " }", 107 | "", 108 | " function saveRecord(context) {", 109 | " ", 110 | " }", 111 | "", 112 | " function validateField(context) {", 113 | " ", 114 | " }", 115 | "", 116 | " function fieldChanged(context) {", 117 | " ", 118 | " }", 119 | "", 120 | " function postSourcing(context) {", 121 | " ", 122 | " }", 123 | "", 124 | " function lineInit(context) {", 125 | " ", 126 | " }", 127 | "", 128 | " function validateDelete(context) {", 129 | " ", 130 | " }", 131 | "", 132 | " function validateInsert(context) {", 133 | " ", 134 | " }", 135 | "", 136 | " function validateLine(context) {", 137 | " ", 138 | " }", 139 | "", 140 | " function sublistChanged(context) {", 141 | " ", 142 | " }", 143 | "", 144 | " return {", 145 | " pageInit: pageInit,", 146 | " saveRecord: saveRecord,", 147 | " validateField: validateField,", 148 | " fieldChanged: fieldChanged,", 149 | " postSourcing: postSourcing,", 150 | " lineInit: lineInit,", 151 | " validateDelete: validateDelete,", 152 | " validateInsert: validateInsert,", 153 | " validateLine: validateLine,", 154 | " sublistChanged: sublistChanged", 155 | " }", 156 | "});", 157 | "" 158 | ] 159 | }, 160 | "defineMapReduce" : { 161 | "prefix": "defineMapReduce", 162 | "description": "Define Map/Reduce script module", 163 | "body": [ 164 | "/**", 165 | " *@NApiVersion 2.x", 166 | " *@NScriptType MapReduceScript", 167 | " */", 168 | "define([], function() {", 169 | "", 170 | " function getInputData() {", 171 | " $0", 172 | " }", 173 | "", 174 | " function map(context) {", 175 | " ", 176 | " }", 177 | "", 178 | " function reduce(context) {", 179 | " ", 180 | " }", 181 | "", 182 | " function summarize(summary) {", 183 | " ", 184 | " }", 185 | "", 186 | " return {", 187 | " getInputData: getInputData,", 188 | " map: map,", 189 | " reduce: reduce,", 190 | " summarize: summarize", 191 | " }", 192 | "});", 193 | "" 194 | ] 195 | }, 196 | "defineScheduled" : { 197 | "prefix": "defineScheduled", 198 | "description": "Define Scheduled script module", 199 | "body": [ 200 | "/**", 201 | " *@NApiVersion 2.x", 202 | " *@NScriptType ScheduledScript", 203 | " */", 204 | "define([], function() {", 205 | "", 206 | " function execute(context) {", 207 | " $0", 208 | " }", 209 | "", 210 | " return {", 211 | " execute: execute", 212 | " }", 213 | "});", 214 | "" 215 | ] 216 | }, 217 | "defineSuitelet" : { 218 | "prefix": "defineSuitelet", 219 | "description": "Define Suitelet script module", 220 | "body": [ 221 | "/**", 222 | " *@NApiVersion 2.x", 223 | " *@NScriptType Suitelet", 224 | " */", 225 | "define([], function() {", 226 | "", 227 | " function onRequest(context) {", 228 | " $0", 229 | " }", 230 | "", 231 | " return {", 232 | " onRequest: onRequest", 233 | " }", 234 | "});", 235 | "" 236 | ] 237 | }, 238 | "defineWorkflowAction" : { 239 | "prefix": "defineWorkflowAction", 240 | "description": "Define Workflow Action script module", 241 | "body": [ 242 | "/**", 243 | " *@NApiVersion 2.x", 244 | " *@NScriptType WorkflowActionScript", 245 | " */", 246 | "define([], function() {", 247 | "", 248 | " function onAction(scriptContext) {", 249 | " $0", 250 | " }", 251 | "", 252 | " return {", 253 | " onAction: onAction", 254 | " }", 255 | "});", 256 | "" 257 | ] 258 | }, 259 | "definePortlet" : { 260 | "prefix": "definePortlet", 261 | "description": "Define Portlet script module", 262 | "body": [ 263 | "/**", 264 | " *@NApiVersion 2.x", 265 | " *@NScriptType Portlet", 266 | " */", 267 | "define([], function() {", 268 | "", 269 | " function render(params) {", 270 | " $0", 271 | " }", 272 | "", 273 | " return {", 274 | " render: render", 275 | " }", 276 | "});", 277 | "" 278 | ] 279 | }, 280 | "defineMassUpdate" : { 281 | "prefix": "defineMassUpdate", 282 | "description": "Define Mass Update script module", 283 | "body": [ 284 | "/**", 285 | " *@NApiVersion 2.0", 286 | " *@NScriptType MassUpdateScript", 287 | " */", 288 | "define([], function() {", 289 | "", 290 | " function each(params) {", 291 | " $0", 292 | " }", 293 | "", 294 | " return {", 295 | " each: each", 296 | " }", 297 | "});", 298 | "" 299 | ] 300 | }, 301 | "defineBundleInstall" : { 302 | "prefix": "defineBundleInstall", 303 | "description": "Define Bundle Installation script module", 304 | "body": [ 305 | "/**", 306 | " *@NApiVersion 2.0", 307 | " *@NScriptType BundleInstallationScript", 308 | " */", 309 | "define([], function() {", 310 | "", 311 | " function checkPrerequisites() {", 312 | " $0if (!runtime.isFeatureInEffect({ feature: 'TIMEOFFMANAGEMENT' }))", 313 | " throw 'The TIMEOFFMANAGEMENT feature must be enabled. Please enable the feature and try again.';", 314 | " }", 315 | "", 316 | " return {", 317 | " beforeInstall: function beforeInstall(params) {", 318 | " checkPrerequisites();", 319 | " },", 320 | " beforeUpdate: function beforeUpdate(params) {", 321 | " checkPrerequisites();", 322 | " }", 323 | " }", 324 | "});", 325 | "" 326 | ] 327 | } 328 | } -------------------------------------------------------------------------------- /test/extension.test.js: -------------------------------------------------------------------------------- 1 | // /* global suite, test */ 2 | 3 | // // 4 | // // Note: This example test is leveraging the Mocha test framework. 5 | // // Please refer to their documentation on https://mochajs.org/ for help. 6 | // // 7 | 8 | // // The module 'assert' provides assertion methods from node 9 | // var assert = require('assert'); 10 | 11 | // // You can import and use all API from the 'vscode' module 12 | // // as well as import your extension to test it 13 | // var vscode = require('vscode'); 14 | // var myExtension = require('../extension'); 15 | 16 | // // Defines a Mocha test suite to group tests of similar kind together 17 | // suite("Extension Tests", function() { 18 | 19 | // // Defines a Mocha unit test 20 | // test("Something 1", function() { 21 | // assert.equal(-1, [1, 2, 3].indexOf(5)); 22 | // assert.equal(-1, [1, 2, 3].indexOf(0)); 23 | // }); 24 | // }); -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING 3 | // 4 | // This file is providing the test runner to use when running extension tests. 5 | // By default the test runner in use is Mocha based. 6 | // 7 | // You can provide your own test runner if you want to override it by exporting 8 | // a function run(testRoot: string, clb: (error:Error) => void) that the extension 9 | // host can call to run the tests. The test runner is expected to use console.log 10 | // to report the results back to the caller. When the tests are finished, return 11 | // a possible error to the callback or null if none. 12 | 13 | var testRunner = require('vscode/lib/testrunner'); 14 | 15 | // You can directly control Mocha options by uncommenting the following lines 16 | // See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info 17 | testRunner.configure({ 18 | ui: 'tdd', // the TDD UI is being used in extension.test.js (suite, test, etc.) 19 | useColors: true // colored output from test results 20 | }); 21 | 22 | module.exports = testRunner; --------------------------------------------------------------------------------