├── .forceignore ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── config └── project-scratch-def.json ├── force-app └── main │ └── default │ └── classes │ ├── RestRoute.cls │ ├── RestRoute.cls-meta.xml │ ├── RestRouteError.cls │ ├── RestRouteError.cls-meta.xml │ ├── RestRouteTestRoutes.cls │ ├── RestRouteTestRoutes.cls-meta.xml │ ├── RestRouteTestUtil.cls │ ├── RestRouteTestUtil.cls-meta.xml │ ├── RestRouteTests.cls │ └── RestRouteTests.cls-meta.xml ├── package-lock.json ├── package.json └── sfdx-project.json /.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__/** -------------------------------------------------------------------------------- /.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 | # Logs 13 | logs 14 | *.log 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | 19 | # Dependency directories 20 | node_modules/ 21 | 22 | # Eslint cache 23 | .eslintcache 24 | 25 | # MacOS system files 26 | .DS_Store 27 | 28 | # Windows system files 29 | Thumbs.db 30 | ehthumbs.db 31 | [Dd]esktop.ini 32 | $RECYCLE.BIN/ 33 | -------------------------------------------------------------------------------- /.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 | .sfdx 6 | .localdevserver -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "tabWidth": 4, 4 | "printWidth": 150, 5 | "useTabs": false, 6 | "overrides": [ 7 | { 8 | "files": "**/lwc/**/*.html", 9 | "options": { 10 | "parser": "lwc" 11 | } 12 | }, 13 | { 14 | "files": "*.{cmp,page,component}", 15 | "options": { 16 | "parser": "html" 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "search.exclude": { 3 | "**/node_modules": true, 4 | "**/bower_components": true, 5 | "**/.sfdx": true 6 | }, 7 | "editor.formatOnSave": true, 8 | "editor.formatOnSaveTimeout": 5000, 9 | "salesforcedx-vscode-core.show-cli-success-msg": false 10 | } 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Charles Jonas 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # apex/:rest/route 2 | 3 | A simple library that allows the creation of RESTful API's. 4 | 5 | ## ✨ Features: 6 | 7 | - supports deeply nested RESTful resource URI 8 | - allows you to focus on the implementation 9 | - automatic responses generation 10 | - error responses to align with Salesforce responses 11 | - flexibility to override most default functionality 12 | - hierarchical composition encourages for code reuse and RESTful design 13 | - lightweight: current implementation is ~ 200LN 14 | 15 | ## 📦 Install 16 | 17 | Via Unlocked Package: [Install Link](https://mydomain.salesforce.com/packaging/installPackage.apexp?p0=04t1C000000goM0QAI) (update `https://mydomain.salesforce.com` for your target org!). 18 | 19 | ## 🔨 Usage 20 | 21 | ### Defining Routes 22 | 23 | Imagine you wanted to create an API to expose the follow resources `Companies` & `CompanyLocations` & `CompanyEmployees` 24 | 25 | [Following RESTful Design](https://hackernoon.com/restful-api-designing-guidelines-the-best-practices-60e1d954e7c9), we might have the following Resource URI definitions: 26 | 27 | - `api/v1/companies` 28 | - `api/v1/companies/:companyId` 29 | 30 | - `api/v1/companies/:companyId/locations` 31 | - `api/v1/companies/:companyId/locations/:locationId` 32 | 33 | - `api/v1/companies/:companyId/employees` 34 | - `api/v1/companies/:companyId/employees/:employeeId` 35 | 36 | To implement this, first we will define our "Routes". 37 | 38 | If you think of the URI as a tree, each Route should correspond to a branch: 39 | 40 | ``` 41 | api/v1 42 | | 43 | companies 44 | | | 45 | locations employees 46 | ``` 47 | 48 | For this example, we will just define three routes: `CompanyRoute`, `CompanyLocationRoute` & `CompanyEmployeeRoute`. 49 | 50 | We could also define a top level route for `api/:versionId`, but for this example we'll just let that be handled by the standard `@RestResource` routing. 51 | 52 | `CompanyRoute` will be responsible for providing a response to the following URI: 53 | 54 | ``` 55 | /api/v1/companies 56 | /api/v1/companies/:companyId 57 | ``` 58 | 59 | `CompanyLocationRoute` will respond to: 60 | 61 | ``` 62 | /api/v1/companies/:companyId/locations 63 | /api/v1/companies/:companyId/locations/:locationId 64 | ``` 65 | 66 | And `CompanyEmployeeRoute` will respond to: 67 | 68 | ``` 69 | /api/v1/companies/:companyId/employees 70 | /api/v1/companies/:companyId/employees/:employeeId 71 | ``` 72 | 73 | ### Implementation 74 | 75 | ```java 76 | @RestResource(urlMapping='/v1/companies/*') 77 | global class CompanyAPI{ 78 | 79 | private static void handleRequest(){ 80 | CompanyRoute router = new CompanyRoute(); 81 | router.execute(); 82 | } 83 | 84 | @HttpGet 85 | global static void handleGet() { 86 | handleRequest(); 87 | } 88 | 89 | @HttpPost 90 | global static void handlePost() { 91 | handleRequest(); 92 | } 93 | 94 | @HttpPut 95 | global static void handlePut() { 96 | handleRequest(); 97 | } 98 | 99 | @HttpDelete 100 | global static void handleDelete() { 101 | handleRequest(); 102 | } 103 | } 104 | ``` 105 | 106 | #### Things to note: 107 | 108 | 1. This `@RestResource` is pretty much just a hook to call into our `CompanyRoute`. 109 | 1. `urlMapping='/api/v1/companies/*'` defines our base route. This should always be the baseUrl for the top level router (`CompanyRoute`), excluding the param. IOW, the `urlMapping` must end with the name of the first route + `/*`. 110 | 111 | ```java 112 | public class CompanyRoute extends RestRoute { 113 | 114 | protected override Object doGet() { 115 | if (!String.isEmpty(this.resourceId)) { 116 | return getCompany(this.resourceId); //implementation not shown 117 | } 118 | return getCompanies(); 119 | } 120 | 121 | protected override Object doPost() { 122 | if (!String.isEmpty(this.param)) { 123 | throw new RestRouteError.RestException('Create Operation does not support Company Identifier', 'NOT_SUPPORTED', 404); 124 | } else { 125 | return createCompany(); 126 | } 127 | } 128 | 129 | //define downstream route 130 | protected override Map getNextRouteMap() { 131 | return new Map{ 132 | 'locations' => new CompanyLocationRoute(this.resourceId), 133 | 'employees' => new CompanyEmployeeRoute(this.resourceId) 134 | }; 135 | } 136 | } 137 | ``` 138 | 139 | #### Things to note: 140 | 141 | 1. Each `RestRoute` route is initialized with a `resourceId` property (if the URI contains one) and `relativePaths` containing the remaining URL paths from the request. 142 | 143 | 1. The `doGet` & `doPost` corresponding to our HTTP methods for this route. Any HTTP method not implement will throw an exception. You can also override `doPut`, `doDelete`. Salesforce does not support `patch` at this time :shrug: 144 | 145 | 1. `getNextRouteMap()` will be used to determine the next RestRoute to call when the URI does not terminate with this Route. The next URI part will be matched against the Map keys. If more advanced routing is needed you can instead override the `next()` method and take full control. 146 | 147 | 1. We pass `this.resourceId` into the next Routes so they have access to `:employeeId`. This composition makes it easy to provide common functionality as lower level routes much pass through their parents. For example, we could query the "Company" and pass that to the next routes instead of just `this.resourceId`. 148 | 149 | ```java 150 | public class CompanyLocationRoute extends RestRoute { 151 | private String companyId; 152 | 153 | public CompanyLocationRoute(String companyId) { 154 | this.companyId = companyId; 155 | } 156 | 157 | protected override Object doGet() { 158 | //filter down by company 159 | CompanyLocation[] companyLocations = getCompanyLocations(companyId); 160 | 161 | if (!String.isEmpty(this.resourceId)) { 162 | return getEntityById(this.resourceId, companyLocations); 163 | } 164 | return companyLocations; 165 | } 166 | } 167 | ``` 168 | 169 | #### Things to note: 170 | 171 | 1. We pass the `companyId` from the above route into the constructor 172 | 1. This route does not implement `next()`. Any requests that don't end terminate with this route will result in a 404 173 | 174 | ### Returning other Content-Types 175 | 176 | By default anything your return from the `doGet()|doPost()|...` methods will be serialized to JSON. However, if you need to respond with another format, you can set `this.response` directly and return `null`: 177 | 178 | ```java 179 | protected override Object doGet() { 180 | this.response.responseBody = Blob.valueOf('Hello World!'); 181 | this.response.addHeader('Content-Type', 'text/plain'); 182 | return null; 183 | } 184 | ``` 185 | 186 | ### Routes without resources... 187 | 188 | While it's not exactly "Restful" you may have routes which do not always following the `/:RESOURCE_URI/:RESOURCE_ID` format. 189 | 190 | For example, if you wanted to implement the following url: 191 | 192 | `/api/v1/other/foo` 193 | 194 | Note that `other` is not followed by a resource ID. If you want to implement `foo` as a RestRoute, then you need to tell `other` not to treat the next URL part as a `:resourceId`. 195 | 196 | To do so, simply override the `hasResource` method: 197 | 198 | ```java 199 | public class OtherRoute extends RestRoute { 200 | protected override boolean hasResource() { 201 | return false; //do parse the next url part as resourceId 202 | } 203 | 204 | protected override Map getNextRouteMap() { 205 | return new Map{ 'foo' => new FooRoute() }; 206 | } 207 | } 208 | ``` 209 | 210 | ### Error Handling 211 | 212 | You can return an Error at anytime by throwing an exception. The `RestError.RestException` allows you to set `StatusCode` and `message` when throwing. There are also build in Errors for common use cases (`RouteNotFoundException` & `RouteNotFoundException`). 213 | 214 | The response body will always contain `List` as this [follows the best practices for handling REST errors](https://salesforce.stackexchange.com/questions/161429/rest-error-handling-design). 215 | 216 | If needed you do change this by overriding `handleException(Exception err)`. 217 | 218 | ### Expanding Results 219 | 220 | With our above example, if we wanted to pull all information about a company we would need to make 3 request: 221 | 222 | > GET /companies/c-1 223 | 224 | ```json 225 | { 226 | "id": "c-1", 227 | "name": "Callaway Cloud" 228 | } 229 | ``` 230 | 231 | > GET /companies/123/employees 232 | 233 | ```json 234 | [ 235 | { 236 | "id": "e-1", 237 | "name": "John Doe", 238 | "role": "Developer" 239 | }, 240 | { 241 | "id": "e-2", 242 | "name": "Billy Jean", 243 | "role": "PM" 244 | } 245 | ] 246 | ``` 247 | 248 | > GET /companies/123/locations 249 | 250 | ```json 251 | [ 252 | { 253 | "id": "l-1", 254 | "name": "Jackson, Wy" 255 | } 256 | ] 257 | ``` 258 | 259 | One interesting bonus of our design is the ability for this library to "expand" results by calling `expandResource(result)`: 260 | 261 | ```java 262 | public override Object doGet() { 263 | Company[] companies = getCompanies(); 264 | if (!String.isEmpty(this.resourceId)) { 265 | Company c = (Company) getEntityById(this.resourceId, companies); 266 | if (this.request.params.containsKey('expand')) { 267 | return expandResource(c); 268 | } 269 | return c; 270 | } 271 | //... collection 272 | } 273 | ``` 274 | 275 | Doing so will run all downstream routes and return a single response with the next level of data! 276 | 277 | ```json 278 | { 279 | "id": "c-1", 280 | "name": "Callaway Cloud", 281 | "employees": [ 282 | { 283 | "id": "e-1", 284 | "name": "John Doe", 285 | "role": "Developer" 286 | }, 287 | { 288 | "id": "e-2", 289 | "name": "Billy Jean", 290 | "role": "PM" 291 | } 292 | ], 293 | "locations": [ 294 | { 295 | "id": "l-1", 296 | "name": "Jackson, Wy" 297 | } 298 | ] 299 | } 300 | ``` 301 | 302 | This works by just running each of the child routes and merging in their data (the property is assigned based on the route; **warning will overwrite any conflict**). 303 | 304 | It is even _possible_ (although a bit more complicated) to expand on collection request. 305 | 306 | ```java 307 | //... doGet() 308 | if (this.request.params.containsKey('expand')) { 309 | List> expandedResponse = new List>(); 310 | for (Company c : companies) { 311 | this.resourceId = c.id; // we must setup state for 312 | expandedResponse.add(expandRecord(c)); 313 | } 314 | return expandedResponse; 315 | } 316 | ``` 317 | 318 | While very cool, expanding collections is generally not advised due it's potential to be highly inefficient. If your downstream routes also support collection expansion, it would recursively continue through the entire tree! 319 | -------------------------------------------------------------------------------- /config/project-scratch-def.json: -------------------------------------------------------------------------------- 1 | { 2 | "orgName": "charlesjonas company", 3 | "edition": "Developer", 4 | "features": [] 5 | } 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/RestRoute.cls: -------------------------------------------------------------------------------- 1 | /** 2 | * Author: charlie jonas (charlie@callaway.cloud) 3 | * Abstract Template for creating hierarchial REST API's. 4 | * Performs one of the following: 5 | * A: Responds when there are no remaining uri parts (relativePaths) 6 | * B: Forwards the request to next router in the tree 7 | * See: https://github.com/ChuckJonas/apex-rest-route for additional documentation 8 | */ 9 | public abstract class RestRoute { 10 | protected RestRequest request { 11 | get { 12 | return RestContext.request; 13 | } 14 | private set; 15 | } 16 | 17 | protected RestResponse response { 18 | get { 19 | return RestContext.response; 20 | } 21 | private set; 22 | } 23 | 24 | protected String[] relativePaths; 25 | public string resourceId; 26 | 27 | private static final String CUSTOM_REST_BASE = '/services/apexrest'; 28 | 29 | public void loadResourceId() { 30 | if (this.relativePaths.size() >= 1) { 31 | this.resourceId = this.popNextPath(); 32 | } 33 | } 34 | 35 | /** 36 | * Runs the Route Tree and sets the RestResponse 37 | * Should ONLY be called from the top level @RestResource RestRoute 38 | */ 39 | public void execute() { 40 | try { 41 | this.parseRelativePath(); 42 | response.addHeader('Content-Type', 'application/json'); 43 | 44 | Object resp = this.route(); 45 | if (resp != null) { 46 | //body may have already been set directly on response object 47 | response.responseBody = Blob.valueOf(JSON.serialize(resp)); 48 | } 49 | if (response.statusCode == null) { 50 | response.statusCode = 200; 51 | } 52 | } catch (Exception e) { 53 | this.handleException(e); 54 | } 55 | } 56 | 57 | // === BEGIN TEMPLATE OVERRIDES === 58 | 59 | /** 60 | * Determines the next route to run. 61 | * Only override when the base standard mapping is not robust enough 62 | * If null is returned, will throw `RouteNotFoundException` 63 | */ 64 | protected virtual RestRoute next() { 65 | String nextPath = popNextPath(); 66 | Map routes = getNextRouteMap(); 67 | if (routes != null && routes.containsKey(nextPath)) { 68 | RestRoute route = routes.get(nextPath); 69 | route.relativePaths = this.relativePaths; 70 | return routes.get(nextPath); 71 | } 72 | throw new RouteNotFoundException(request); 73 | } 74 | 75 | /** 76 | * Can be override to prevent popping the next URL part to the ResourceId 77 | */ 78 | protected virtual boolean hasResource() { 79 | return true; 80 | } 81 | 82 | /** 83 | * Standard Route Mapping 84 | * Key of String will be matched against the next URI path 85 | */ 86 | protected virtual Map getNextRouteMap() { 87 | return null; 88 | } 89 | 90 | /** 91 | * Handle Error 92 | * Follows https://salesforce.stackexchange.com/questions/161429/rest-error-handling-design 93 | */ 94 | protected virtual void handleException(Exception err) { 95 | if (err instanceof RestRouteError.RestException) { 96 | RestRouteError.RestException restErr = (RestRouteError.RestException) err; 97 | response.statusCode = restErr.statusCode; 98 | response.responseBody = Blob.valueOf(JSON.serialize(new List{ restErr.errorResp })); 99 | } else { 100 | throw err; //let salesforce deal with it 101 | } 102 | } 103 | 104 | /** 105 | * Determines the next route to run. 106 | * Do not need to override if route is a leaf. 107 | * If null is returned, will throw `RouteNotFoundException` 108 | */ 109 | protected virtual Object doGet() { 110 | throw new OperationNotSupportException(request); 111 | } 112 | protected virtual Object doPost() { 113 | throw new OperationNotSupportException(request); 114 | } 115 | protected virtual Object doDelete() { 116 | throw new OperationNotSupportException(request); 117 | } 118 | protected virtual Object doPut() { 119 | throw new OperationNotSupportException(request); 120 | } 121 | 122 | // === END TEMPLATE OVERRIDES === 123 | 124 | //either responds to the request, or forwards it to the next RestRoute 125 | protected Object route() { 126 | System.debug(this.relativePaths); 127 | if (this.hasResource()) { 128 | this.loadResourceId(); 129 | } 130 | 131 | if (!this.hasNextPath()) { 132 | return this.respond(); 133 | } 134 | RestRoute nextRoute = this.next(); 135 | if (nextRoute != null) { 136 | return nextRoute.route(); 137 | } 138 | throw new RestRoute.RouteNotFoundException(request); 139 | } 140 | 141 | // run appropriate HTTP METHOD 142 | protected Object respond() { 143 | System.debug(request.httpMethod); 144 | switch on request.httpMethod { 145 | when 'GET' { 146 | return this.doGet(); 147 | } 148 | when 'POST' { 149 | return this.doPost(); 150 | } 151 | when 'DELETE' { 152 | return this.doDelete(); 153 | } 154 | when 'PUT' { 155 | return this.doPut(); 156 | } 157 | when else { 158 | throw new RouteNotFoundException(request); 159 | } 160 | } 161 | } 162 | 163 | /** 164 | * returns true if there are more relative URI paths 165 | */ 166 | protected Boolean hasNextPath() { 167 | return relativePaths.size() > 0; 168 | } 169 | 170 | /** 171 | * pops the next relative Uri path 172 | */ 173 | protected String popNextPath() { 174 | return relativePaths.remove(0); 175 | } 176 | 177 | protected String peakNextPath() { 178 | return relativePaths[0]; 179 | } 180 | 181 | /** 182 | * Runs all child routes and merges their results in the result object 183 | */ 184 | protected Map expandResource(Object result) { 185 | Map expandedResult = (Map) JSON.deserializeUntyped(JSON.serialize(result)); 186 | Map routes = getNextRouteMap(); 187 | for (String key : routes.keySet()) { 188 | expandedResult.put(key, routes.get(key).respond()); 189 | } 190 | return expandedResult; 191 | } 192 | 193 | // Sets up relativePaths on entry route 194 | // Only run on execute() 195 | private void parseRelativePath() { 196 | // init relative paths 197 | String basePath = this.request.resourcePath.replace(CUSTOM_REST_BASE, '').replace('/*', ''); 198 | 199 | this.relativePaths = this.request.requestURI.replace(basePath, '').split('\\/'); 200 | if (relativePaths.size() > 0 && String.isBlank(relativePaths.get(0))) { 201 | popNextPath(); 202 | } 203 | } 204 | 205 | public class RouteNotFoundException extends RestRouteError.RestException { 206 | public RouteNotFoundException(RestRequest req) { 207 | super('Could not find route for: ' + req.requestURI, 'NOT_FOUND', 404); 208 | } 209 | } 210 | 211 | public class OperationNotSupportException extends RestRouteError.RestException { 212 | public OperationNotSupportException(RestRequest req) { 213 | super('Method is not supported ' + req.httpMethod, 'NOT_SUPPORTED', 400); 214 | } 215 | } 216 | 217 | public class EntityNotFoundException extends RestRouteError.RestException { 218 | public EntityNotFoundException(RestRequest req, String resourceId) { 219 | super('Entity does not exist: ' + resourceId, 'NOT_FOUND', 404); 220 | } 221 | } 222 | 223 | // useful for stubbing routing 224 | public class NotImplementedRoute extends RestRoute { 225 | public NotImplementedRoute() { 226 | super(); 227 | } 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /force-app/main/default/classes/RestRoute.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 47.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/RestRouteError.cls: -------------------------------------------------------------------------------- 1 | public class RestRouteError { 2 | public class Response { 3 | public String errorCode; 4 | public String message; 5 | public Response(String errorCode, string message) { 6 | this.errorCode = errorCode; 7 | this.message = message; 8 | } 9 | } 10 | 11 | public virtual class RestException extends Exception { 12 | public Response errorResp; 13 | public Integer statusCode; 14 | public RestException(String message, String errorCode, Integer statusCode) { 15 | this.setMessage(message); 16 | this.errorResp = new Response(errorCode, message); 17 | this.statusCode = statusCode; 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /force-app/main/default/classes/RestRouteError.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 47.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/RestRouteTestRoutes.cls: -------------------------------------------------------------------------------- 1 | @isTest 2 | public class RestRouteTestRoutes { 3 | //TOP LEVEL ROUTE 4 | 5 | // could be used to support multiple versions 6 | public class APIEntryRoute extends RestRoute { 7 | protected override Map getNextRouteMap() { 8 | //two ways to support multiple versions 9 | // 1. Return different routers based on this.resourceId 10 | // 2. Pass this.resourceId into children routes and let each route deal 11 | // with how to respond to different versions 12 | return new Map{ 13 | 'companies' => new CompanyRoute(), 14 | 'employees' => new CompanyEmployeeRoute(null), 15 | 'other' => new OtherRoute() 16 | }; 17 | } 18 | } 19 | 20 | // other/foo/ 21 | public class OtherRoute extends RestRoute { 22 | protected override boolean hasResource() { 23 | return false; 24 | } 25 | protected override Map getNextRouteMap() { 26 | return new Map{ 'foo' => new FooRoute() }; 27 | } 28 | } 29 | 30 | public class FooRoute extends RestRoute { 31 | public override Object doGet() { 32 | response.responseBody = Blob.valueOf('foo'); 33 | return null; 34 | } 35 | } 36 | 37 | // companies/:companyId? 38 | public class CompanyRoute extends RestRoute { 39 | public override Object doGet() { 40 | Company[] companies = getCompanies(); 41 | if (!String.isEmpty(this.resourceId)) { 42 | Company c = (Company) getEntityById(this.resourceId, companies); 43 | if (this.request.params.containsKey('expand')) { 44 | return expandResource(c); 45 | } 46 | return c; 47 | } 48 | 49 | if (this.request.params.containsKey('expand')) { 50 | List> expandedResponse = new List>(); 51 | for (Company c : companies) { 52 | this.resourceId = c.id; 53 | expandedResponse.add(expandResource(c)); 54 | } 55 | return expandedResponse; 56 | } 57 | 58 | return companies; 59 | } 60 | 61 | protected override Map getNextRouteMap() { 62 | return new Map{ 63 | 'locations' => new CompanyLocationRoute(this.resourceId), 64 | 'employees' => new CompanyEmployeeRoute(this.resourceId) 65 | }; 66 | } 67 | } 68 | 69 | // Company Locations Route 70 | // companies/:companyId/locations/:locationId? 71 | public class CompanyLocationRoute extends RestRoute { 72 | private String companyId; 73 | 74 | public CompanyLocationRoute(String companyId) { 75 | this.companyId = companyId; 76 | } 77 | 78 | protected override Object doGet() { 79 | //filter down by company 80 | CompanyEntity[] companyLocations = getEntitiesByCompany(companyId, LOCATIONS); 81 | 82 | if (!String.isEmpty(this.resourceId)) { 83 | return getEntityById(this.resourceId, companyLocations); 84 | } 85 | return companyLocations; 86 | } 87 | } 88 | 89 | // Company Locations Route 90 | // api/v1/employees?companyId=:companyId 91 | // api/v1/companies/:companyId/employees/:employeeId? 92 | public class CompanyEmployeeRoute extends RestRoute { 93 | private String companyId; 94 | 95 | public CompanyEmployeeRoute(String companyId) { 96 | this.companyId = companyId; 97 | } 98 | 99 | protected override Object doGet() { 100 | //support for /api/v1/employees?companyId=:companyId 101 | if (this.companyId == null && this.request.params.containsKey('companyId')) { 102 | this.companyId = this.request.params.get('companyId'); 103 | } 104 | 105 | //optionally filter down by company 106 | CompanyEntity[] companyEmployees = EMPLOYEES; 107 | if (this.companyId != null) { 108 | companyEmployees = getEntitiesByCompany(this.companyId, EMPLOYEES); 109 | } 110 | 111 | if (!String.isEmpty(this.resourceId)) { 112 | return getEntityById(this.resourceId, companyEmployees); 113 | } 114 | 115 | if (this.request.params.containsKey('role')) { 116 | String roleFilter = this.request.params.get('role'); 117 | for (Integer i = companyEmployees.size() - 1; i >= 0; i--) { 118 | CompanyEmployee employee = (CompanyEmployee) companyEmployees[i]; 119 | if (employee.role != roleFilter) { 120 | companyEmployees.remove(i); 121 | } 122 | } 123 | } 124 | return companyEmployees; 125 | } 126 | } 127 | 128 | // DATA MODEL & RETRIEVAL HELPERS 129 | public virtual class IdEntity { 130 | public string id; 131 | public IdEntity(String id) { 132 | this.id = id; 133 | } 134 | } 135 | 136 | public class Company extends IdEntity { 137 | public String name; 138 | public Company(String id, String name) { 139 | super(id); 140 | this.name = name; 141 | } 142 | } 143 | 144 | public virtual class CompanyEntity extends IdEntity { 145 | public String companyId; 146 | public CompanyEntity(String companyId, String id) { 147 | super(id); 148 | this.companyId = companyId; 149 | } 150 | } 151 | 152 | public class CompanyLocation extends CompanyEntity { 153 | public String name; 154 | public CompanyLocation(String companyId, String id, String name) { 155 | super(companyId, id); 156 | this.name = name; 157 | } 158 | } 159 | 160 | public class CompanyEmployee extends CompanyEntity { 161 | public String name; 162 | public String role; 163 | public CompanyEmployee(String companyId, String id, String name, String role) { 164 | super(companyId, id); 165 | this.name = name; 166 | this.role = role; 167 | } 168 | } 169 | 170 | // === DATA + HELPERS === 171 | 172 | static Company[] getCompanies() { 173 | return COMPANIES; 174 | } 175 | 176 | static final Company[] COMPANIES = new List{ new Company('c-1', 'Acme'), new Company('c-2', 'Stark Industries') }; 177 | 178 | static final CompanyLocation[] LOCATIONS = new List{ 179 | new CompanyLocation('c-1', 'l-1', 'Desert'), 180 | new CompanyLocation('c-1', 'l-2', 'NYC'), 181 | new CompanyLocation('c-2', 'l-3', 'LA') 182 | }; 183 | 184 | static final CompanyEmployee[] EMPLOYEES = new List{ 185 | new CompanyEmployee('c-1', 'e-1', 'Daffy Duck', 'CEO'), 186 | new CompanyEmployee('c-1', 'e-2', 'Bugs Bunny', 'CFO'), 187 | new CompanyEmployee('c-2', 'e-3', 'Iron Man', 'CEO'), 188 | new CompanyEmployee('c-2', 'e-4', 'Tony Stark', 'CEO'), 189 | new CompanyEmployee('c-2', 'e-5', 'Test Dummy', 'QA') 190 | }; 191 | 192 | private static IdEntity getEntityById(String id, IdEntity[] entities) { 193 | for (IdEntity c : entities) { 194 | if (c.id == id) { 195 | return c; 196 | } 197 | } 198 | throw new RestRoute.EntityNotFoundException(null, id); 199 | } 200 | 201 | private static CompanyEntity[] getEntitiesByCompany(String companyId, CompanyEntity[] entities) { 202 | CompanyEntity[] companyEntities = new List{}; 203 | for (CompanyEntity entity : entities) { 204 | if (entity.companyId == companyId) { 205 | companyEntities.add(entity); 206 | } 207 | } 208 | return companyEntities; 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /force-app/main/default/classes/RestRouteTestRoutes.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 47.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/RestRouteTestUtil.cls: -------------------------------------------------------------------------------- 1 | // Test helpers for RestRoute 2 | public class RestRouteTestUtil { 3 | //shorthand simple GET REQUEST 4 | public static void setupRestContext(String resourcePath, String requestUri) { 5 | setupRestContext(resourcePath, requestUri, 'GET', null, null); 6 | } 7 | 8 | //shorthand for GET + URL Params 9 | public static void setupRestContext(String resourcePath, String requestUri, Map query) { 10 | setupRestContext(resourcePath, requestUri, 'GET', query, null); 11 | } 12 | 13 | public static void setupRestContext( 14 | String resourcePath, 15 | String requestUri, 16 | String httpMethod, 17 | Map query, 18 | Blob body 19 | ) { 20 | RestRequest req = new RestRequest(); 21 | req.resourcePath = resourcePath; 22 | req.requestURI = requestUri; 23 | req.httpMethod = httpMethod; 24 | if (query != null) { 25 | req.params.putAll(query); 26 | } 27 | if (body != null) { 28 | req.requestBody = body; 29 | } 30 | 31 | RestContext.Request = req; 32 | RestContext.Response = new RestResponse(); 33 | } 34 | 35 | public static RestRouteError.Response[] parseResponseErrors(RestResponse resp) { 36 | return (RestRouteError.Response[]) JSON.deserialize( 37 | resp.responseBody.toString(), 38 | List.class 39 | ); 40 | } 41 | } -------------------------------------------------------------------------------- /force-app/main/default/classes/RestRouteTestUtil.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 47.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/RestRouteTests.cls: -------------------------------------------------------------------------------- 1 | @isTest 2 | public class RestRouteTests { 3 | private static final String RESOURCE_URL = '/api/*'; 4 | private static final String RESOURCE_PATH = '/services/apexrest' + RESOURCE_URL; 5 | 6 | @isTest 7 | public static void testEntryRoute() { 8 | RestRouteTestUtil.setupRestContext(RESOURCE_PATH, '/api/v1'); 9 | RestResponse resp = RestContext.Response; 10 | 11 | RestRouteTestRoutes.APIEntryRoute router = new RestRouteTestRoutes.APIEntryRoute(); 12 | router.execute(); 13 | 14 | System.assertEquals(400, resp.statusCode); 15 | } 16 | 17 | @isTest 18 | public static void testEntryRouteCollections() { 19 | RestRouteTestUtil.setupRestContext(RESOURCE_PATH, '/api'); 20 | RestResponse resp = RestContext.Response; 21 | 22 | RestRouteTestRoutes.APIEntryRoute router = new RestRouteTestRoutes.APIEntryRoute(); 23 | router.execute(); 24 | 25 | System.assertEquals(400, resp.statusCode); 26 | } 27 | 28 | @isTest 29 | public static void testBaseRouteCollection() { 30 | RestRouteTestUtil.setupRestContext(RESOURCE_PATH, '/api/v1/companies/'); 31 | RestResponse resp = RestContext.Response; 32 | 33 | RestRouteTestRoutes.APIEntryRoute router = new RestRouteTestRoutes.APIEntryRoute(); 34 | router.execute(); 35 | 36 | System.assertEquals(200, resp.statusCode); 37 | RestRouteTestRoutes.Company[] respData = (RestRouteTestRoutes.Company[]) JSON.deserialize( 38 | resp.responseBody.toString(), 39 | List.class 40 | ); 41 | 42 | System.assertEquals(2, respData.size()); 43 | } 44 | 45 | @isTest 46 | public static void testNoResourceRoute() { 47 | RestRouteTestUtil.setupRestContext(RESOURCE_PATH, '/api/v1/other/foo'); 48 | RestResponse resp = RestContext.Response; 49 | 50 | RestRouteTestRoutes.APIEntryRoute router = new RestRouteTestRoutes.APIEntryRoute(); 51 | router.execute(); 52 | 53 | System.assertEquals(200, resp.statusCode); 54 | 55 | System.assertEquals('foo', resp.responseBody.toString()); 56 | } 57 | 58 | @isTest 59 | public static void testBaseRouteCollection2() { 60 | RestRouteTestUtil.setupRestContext(RESOURCE_PATH, '/api/v1/companies'); 61 | RestResponse resp = RestContext.Response; 62 | 63 | RestRouteTestRoutes.APIEntryRoute router = new RestRouteTestRoutes.APIEntryRoute(); 64 | router.execute(); 65 | 66 | System.assertEquals(200, resp.statusCode); 67 | RestRouteTestRoutes.Company[] respData = (RestRouteTestRoutes.Company[]) JSON.deserialize( 68 | resp.responseBody.toString(), 69 | List.class 70 | ); 71 | 72 | System.assertEquals(2, respData.size()); 73 | } 74 | 75 | @isTest 76 | public static void testBaseRouteSingle() { 77 | RestRouteTestUtil.setupRestContext(RESOURCE_PATH, '/api/v1/companies/c-1'); 78 | RestResponse resp = RestContext.Response; 79 | 80 | RestRouteTestRoutes.APIEntryRoute router = new RestRouteTestRoutes.APIEntryRoute(); 81 | router.execute(); 82 | 83 | System.assertEquals(200, resp.statusCode); 84 | RestRouteTestRoutes.Company respData = (RestRouteTestRoutes.Company) JSON.deserialize( 85 | resp.responseBody.toString(), 86 | RestRouteTestRoutes.Company.class 87 | ); 88 | 89 | System.assertEquals('c-1', respData.id); 90 | } 91 | 92 | @isTest 93 | public static void testSingleExpanded() { 94 | RestRouteTestUtil.setupRestContext(RESOURCE_PATH, '/api/v1/companies/c-1', new Map{ 'expand' => '1' }); 95 | RestResponse resp = RestContext.Response; 96 | 97 | RestRouteTestRoutes.APIEntryRoute router = new RestRouteTestRoutes.APIEntryRoute(); 98 | router.execute(); 99 | 100 | System.assertEquals(200, resp.statusCode); 101 | 102 | // TODO make useful assertions 103 | // System.assertEquals('', resp.responseBody.toString()); 104 | } 105 | 106 | @isTest 107 | public static void testCollectionExpanded() { 108 | RestRouteTestUtil.setupRestContext(RESOURCE_PATH, '/api/v1/companies', new Map{ 'expand' => '1' }); 109 | RestResponse resp = RestContext.Response; 110 | 111 | RestRouteTestRoutes.APIEntryRoute router = new RestRouteTestRoutes.APIEntryRoute(); 112 | router.execute(); 113 | 114 | System.assertEquals(200, resp.statusCode); 115 | 116 | // TODO make useful assertions 117 | // System.assertEquals('', resp.responseBody.toString()); 118 | } 119 | 120 | @isTest 121 | public static void testNestedRouteCollection() { 122 | RestRouteTestUtil.setupRestContext(RESOURCE_PATH, '/api/v1/companies/c-2/locations'); 123 | RestResponse resp = RestContext.Response; 124 | 125 | RestRouteTestRoutes.APIEntryRoute router = new RestRouteTestRoutes.APIEntryRoute(); 126 | router.execute(); 127 | 128 | System.assertEquals(200, resp.statusCode); 129 | RestRouteTestRoutes.CompanyLocation[] respData = (RestRouteTestRoutes.CompanyLocation[]) JSON.deserialize( 130 | resp.responseBody.toString(), 131 | List.class 132 | ); 133 | 134 | System.assertEquals(1, respData.size()); 135 | System.assertEquals('LA', respData[0].name); 136 | } 137 | 138 | @isTest 139 | public static void testAlternateRoute() { 140 | RestRouteTestUtil.setupRestContext(RESOURCE_PATH, '/api/v1/employees', new Map{ 'companyId' => 'c-2' }); 141 | RestResponse resp = RestContext.Response; 142 | 143 | RestRouteTestRoutes.APIEntryRoute router = new RestRouteTestRoutes.APIEntryRoute(); 144 | router.execute(); 145 | 146 | System.assertEquals(200, resp.statusCode); 147 | RestRouteTestRoutes.CompanyEmployee[] respData = (RestRouteTestRoutes.CompanyEmployee[]) JSON.deserialize( 148 | resp.responseBody.toString(), 149 | List.class 150 | ); 151 | 152 | System.assertEquals(3, respData.size()); 153 | System.assertEquals('Iron Man', respData[0].name); 154 | } 155 | 156 | @isTest 157 | public static void testRouteCollectionFilter() { 158 | RestRouteTestUtil.setupRestContext(RESOURCE_PATH, '/api/v1/companies/c-2/employees', new Map{ 'role' => 'CEO' }); 159 | RestResponse resp = RestContext.Response; 160 | 161 | RestRouteTestRoutes.APIEntryRoute router = new RestRouteTestRoutes.APIEntryRoute(); 162 | router.execute(); 163 | 164 | System.assertEquals(200, resp.statusCode); 165 | RestRouteTestRoutes.CompanyEmployee[] respData = (RestRouteTestRoutes.CompanyEmployee[]) JSON.deserialize( 166 | resp.responseBody.toString(), 167 | List.class 168 | ); 169 | 170 | System.assertEquals(2, respData.size()); 171 | System.assertEquals('Iron Man', respData[0].name); 172 | } 173 | 174 | @isTest 175 | public static void testNestedRouteSingle() { 176 | RestRouteTestUtil.setupRestContext(RESOURCE_PATH, '/api/v1/companies/c-1/employees/e-2'); 177 | RestResponse resp = RestContext.Response; 178 | 179 | RestRouteTestRoutes.APIEntryRoute router = new RestRouteTestRoutes.APIEntryRoute(); 180 | router.execute(); 181 | 182 | System.assertEquals(200, resp.statusCode); 183 | RestRouteTestRoutes.CompanyEmployee respData = (RestRouteTestRoutes.CompanyEmployee) JSON.deserialize( 184 | resp.responseBody.toString(), 185 | RestRouteTestRoutes.CompanyEmployee.class 186 | ); 187 | 188 | System.assertEquals('e-2', respData.id); 189 | } 190 | 191 | @isTest 192 | public static void testNestedRouteSingleNotRelated() { 193 | RestRouteTestUtil.setupRestContext(RESOURCE_PATH, '/api/v1/companies/c-2/employees/e-2'); 194 | RestResponse resp = RestContext.Response; 195 | 196 | RestRouteTestRoutes.APIEntryRoute router = new RestRouteTestRoutes.APIEntryRoute(); 197 | router.execute(); 198 | 199 | System.assertEquals(404, resp.statusCode); 200 | RestRouteError.Response[] respData = RestRouteTestUtil.parseResponseErrors(resp); 201 | 202 | System.assertEquals(1, respData.size()); 203 | System.assertEquals('NOT_FOUND', respData[0].errorCode); 204 | } 205 | 206 | @isTest 207 | public static void testNoRouteFound() { 208 | RestRouteTestUtil.setupRestContext(RESOURCE_PATH, '/api/v1/companies/c-2/asdf'); 209 | RestResponse resp = RestContext.Response; 210 | 211 | RestRouteTestRoutes.APIEntryRoute router = new RestRouteTestRoutes.APIEntryRoute(); 212 | router.execute(); 213 | 214 | System.assertEquals(404, resp.statusCode); 215 | RestRouteError.Response[] respData = RestRouteTestUtil.parseResponseErrors(resp); 216 | 217 | System.assertEquals(1, respData.size()); 218 | System.assertEquals('NOT_FOUND', respData[0].errorCode); 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /force-app/main/default/classes/RestRouteTests.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 47.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apex-rest-route", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@hapi/address": { 8 | "version": "2.1.4", 9 | "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz", 10 | "integrity": "sha512-QD1PhQk+s31P1ixsX0H0Suoupp3VMXzIVMSwobR3F3MSUO2YCV0B7xqLcUw/Bh8yuvd3LhpyqLQWTNcRmp6IdQ==", 11 | "dev": true 12 | }, 13 | "@hapi/bourne": { 14 | "version": "1.3.2", 15 | "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-1.3.2.tgz", 16 | "integrity": "sha512-1dVNHT76Uu5N3eJNTYcvxee+jzX4Z9lfciqRRHCU27ihbUcYi+iSc2iml5Ke1LXe1SyJCLA0+14Jh4tXJgOppA==", 17 | "dev": true 18 | }, 19 | "@hapi/hoek": { 20 | "version": "8.5.0", 21 | "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-8.5.0.tgz", 22 | "integrity": "sha512-7XYT10CZfPsH7j9F1Jmg1+d0ezOux2oM2GfArAzLwWe4mE2Dr3hVjsAL6+TFY49RRJlCdJDMw3nJsLFroTc8Kw==", 23 | "dev": true 24 | }, 25 | "@hapi/joi": { 26 | "version": "15.1.1", 27 | "resolved": "https://registry.npmjs.org/@hapi/joi/-/joi-15.1.1.tgz", 28 | "integrity": "sha512-entf8ZMOK8sc+8YfeOlM8pCfg3b5+WZIKBfUaaJT8UsjAAPjartzxIYm3TIbjvA4u+u++KbcXD38k682nVHDAQ==", 29 | "dev": true, 30 | "requires": { 31 | "@hapi/address": "2.x.x", 32 | "@hapi/bourne": "1.x.x", 33 | "@hapi/hoek": "8.x.x", 34 | "@hapi/topo": "3.x.x" 35 | } 36 | }, 37 | "@hapi/topo": { 38 | "version": "3.1.6", 39 | "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-3.1.6.tgz", 40 | "integrity": "sha512-tAag0jEcjwH+P2quUfipd7liWCNX2F8NvYjQp2wtInsZxnMlypdw0FtAOLxtvvkO+GSRRbmNi8m/5y42PQJYCQ==", 41 | "dev": true, 42 | "requires": { 43 | "@hapi/hoek": "^8.3.0" 44 | } 45 | }, 46 | "ajv": { 47 | "version": "6.11.0", 48 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.11.0.tgz", 49 | "integrity": "sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA==", 50 | "dev": true, 51 | "requires": { 52 | "fast-deep-equal": "^3.1.1", 53 | "fast-json-stable-stringify": "^2.0.0", 54 | "json-schema-traverse": "^0.4.1", 55 | "uri-js": "^4.2.2" 56 | } 57 | }, 58 | "ansi-regex": { 59 | "version": "4.1.0", 60 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", 61 | "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", 62 | "dev": true 63 | }, 64 | "ansi-styles": { 65 | "version": "3.2.1", 66 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 67 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 68 | "dev": true, 69 | "requires": { 70 | "color-convert": "^1.9.0" 71 | } 72 | }, 73 | "asn1": { 74 | "version": "0.2.4", 75 | "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", 76 | "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", 77 | "dev": true, 78 | "requires": { 79 | "safer-buffer": "~2.1.0" 80 | } 81 | }, 82 | "assert-plus": { 83 | "version": "1.0.0", 84 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 85 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", 86 | "dev": true 87 | }, 88 | "asynckit": { 89 | "version": "0.4.0", 90 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 91 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", 92 | "dev": true 93 | }, 94 | "aws-sign2": { 95 | "version": "0.7.0", 96 | "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", 97 | "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", 98 | "dev": true 99 | }, 100 | "aws4": { 101 | "version": "1.9.1", 102 | "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz", 103 | "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==", 104 | "dev": true 105 | }, 106 | "axios": { 107 | "version": "0.19.2", 108 | "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", 109 | "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", 110 | "dev": true, 111 | "requires": { 112 | "follow-redirects": "1.5.10" 113 | } 114 | }, 115 | "bcrypt-pbkdf": { 116 | "version": "1.0.2", 117 | "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", 118 | "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", 119 | "dev": true, 120 | "requires": { 121 | "tweetnacl": "^0.14.3" 122 | } 123 | }, 124 | "camelcase": { 125 | "version": "5.3.1", 126 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", 127 | "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", 128 | "dev": true 129 | }, 130 | "caseless": { 131 | "version": "0.12.0", 132 | "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", 133 | "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", 134 | "dev": true 135 | }, 136 | "cliui": { 137 | "version": "5.0.0", 138 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", 139 | "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", 140 | "dev": true, 141 | "requires": { 142 | "string-width": "^3.1.0", 143 | "strip-ansi": "^5.2.0", 144 | "wrap-ansi": "^5.1.0" 145 | } 146 | }, 147 | "color-convert": { 148 | "version": "1.9.3", 149 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 150 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 151 | "dev": true, 152 | "requires": { 153 | "color-name": "1.1.3" 154 | } 155 | }, 156 | "color-name": { 157 | "version": "1.1.3", 158 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 159 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", 160 | "dev": true 161 | }, 162 | "combined-stream": { 163 | "version": "1.0.8", 164 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 165 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 166 | "dev": true, 167 | "requires": { 168 | "delayed-stream": "~1.0.0" 169 | } 170 | }, 171 | "core-js": { 172 | "version": "2.6.11", 173 | "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", 174 | "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==", 175 | "dev": true 176 | }, 177 | "core-util-is": { 178 | "version": "1.0.2", 179 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 180 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", 181 | "dev": true 182 | }, 183 | "dashdash": { 184 | "version": "1.14.1", 185 | "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", 186 | "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", 187 | "dev": true, 188 | "requires": { 189 | "assert-plus": "^1.0.0" 190 | } 191 | }, 192 | "debug": { 193 | "version": "3.1.0", 194 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 195 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 196 | "dev": true, 197 | "requires": { 198 | "ms": "2.0.0" 199 | } 200 | }, 201 | "decamelize": { 202 | "version": "1.2.0", 203 | "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", 204 | "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", 205 | "dev": true 206 | }, 207 | "delayed-stream": { 208 | "version": "1.0.0", 209 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 210 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", 211 | "dev": true 212 | }, 213 | "detect-newline": { 214 | "version": "2.1.0", 215 | "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", 216 | "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=", 217 | "dev": true 218 | }, 219 | "ecc-jsbn": { 220 | "version": "0.1.2", 221 | "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", 222 | "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", 223 | "dev": true, 224 | "requires": { 225 | "jsbn": "~0.1.0", 226 | "safer-buffer": "^2.1.0" 227 | } 228 | }, 229 | "emoji-regex": { 230 | "version": "7.0.3", 231 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", 232 | "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", 233 | "dev": true 234 | }, 235 | "extend": { 236 | "version": "3.0.2", 237 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", 238 | "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", 239 | "dev": true 240 | }, 241 | "extsprintf": { 242 | "version": "1.3.0", 243 | "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", 244 | "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", 245 | "dev": true 246 | }, 247 | "fast-deep-equal": { 248 | "version": "3.1.1", 249 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", 250 | "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", 251 | "dev": true 252 | }, 253 | "fast-json-stable-stringify": { 254 | "version": "2.1.0", 255 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 256 | "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", 257 | "dev": true 258 | }, 259 | "find-up": { 260 | "version": "3.0.0", 261 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", 262 | "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", 263 | "dev": true, 264 | "requires": { 265 | "locate-path": "^3.0.0" 266 | } 267 | }, 268 | "follow-redirects": { 269 | "version": "1.5.10", 270 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", 271 | "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", 272 | "dev": true, 273 | "requires": { 274 | "debug": "=3.1.0" 275 | } 276 | }, 277 | "forever-agent": { 278 | "version": "0.6.1", 279 | "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", 280 | "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", 281 | "dev": true 282 | }, 283 | "form-data": { 284 | "version": "2.3.3", 285 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", 286 | "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", 287 | "dev": true, 288 | "requires": { 289 | "asynckit": "^0.4.0", 290 | "combined-stream": "^1.0.6", 291 | "mime-types": "^2.1.12" 292 | } 293 | }, 294 | "get-caller-file": { 295 | "version": "2.0.5", 296 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 297 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", 298 | "dev": true 299 | }, 300 | "getpass": { 301 | "version": "0.1.7", 302 | "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", 303 | "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", 304 | "dev": true, 305 | "requires": { 306 | "assert-plus": "^1.0.0" 307 | } 308 | }, 309 | "har-schema": { 310 | "version": "2.0.0", 311 | "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", 312 | "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", 313 | "dev": true 314 | }, 315 | "har-validator": { 316 | "version": "5.1.3", 317 | "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", 318 | "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", 319 | "dev": true, 320 | "requires": { 321 | "ajv": "^6.5.5", 322 | "har-schema": "^2.0.0" 323 | } 324 | }, 325 | "http-signature": { 326 | "version": "1.2.0", 327 | "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", 328 | "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", 329 | "dev": true, 330 | "requires": { 331 | "assert-plus": "^1.0.0", 332 | "jsprim": "^1.2.2", 333 | "sshpk": "^1.7.0" 334 | } 335 | }, 336 | "is-fullwidth-code-point": { 337 | "version": "2.0.0", 338 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", 339 | "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", 340 | "dev": true 341 | }, 342 | "is-typedarray": { 343 | "version": "1.0.0", 344 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 345 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", 346 | "dev": true 347 | }, 348 | "isstream": { 349 | "version": "0.1.2", 350 | "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", 351 | "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", 352 | "dev": true 353 | }, 354 | "jest-docblock": { 355 | "version": "24.9.0", 356 | "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-24.9.0.tgz", 357 | "integrity": "sha512-F1DjdpDMJMA1cN6He0FNYNZlo3yYmOtRUnktrT9Q37njYzC5WEaDdmbynIgy0L/IvXvvgsG8OsqhLPXTpfmZAA==", 358 | "dev": true, 359 | "requires": { 360 | "detect-newline": "^2.1.0" 361 | } 362 | }, 363 | "jsbn": { 364 | "version": "0.1.1", 365 | "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", 366 | "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", 367 | "dev": true 368 | }, 369 | "json-schema": { 370 | "version": "0.2.3", 371 | "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", 372 | "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", 373 | "dev": true 374 | }, 375 | "json-schema-traverse": { 376 | "version": "0.4.1", 377 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 378 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", 379 | "dev": true 380 | }, 381 | "json-stringify-safe": { 382 | "version": "5.0.1", 383 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 384 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", 385 | "dev": true 386 | }, 387 | "jsprim": { 388 | "version": "1.4.1", 389 | "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", 390 | "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", 391 | "dev": true, 392 | "requires": { 393 | "assert-plus": "1.0.0", 394 | "extsprintf": "1.3.0", 395 | "json-schema": "0.2.3", 396 | "verror": "1.10.0" 397 | } 398 | }, 399 | "locate-path": { 400 | "version": "3.0.0", 401 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", 402 | "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", 403 | "dev": true, 404 | "requires": { 405 | "p-locate": "^3.0.0", 406 | "path-exists": "^3.0.0" 407 | } 408 | }, 409 | "mime-db": { 410 | "version": "1.43.0", 411 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz", 412 | "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==", 413 | "dev": true 414 | }, 415 | "mime-types": { 416 | "version": "2.1.26", 417 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz", 418 | "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==", 419 | "dev": true, 420 | "requires": { 421 | "mime-db": "1.43.0" 422 | } 423 | }, 424 | "minimist": { 425 | "version": "1.2.0", 426 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", 427 | "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", 428 | "dev": true 429 | }, 430 | "ms": { 431 | "version": "2.0.0", 432 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 433 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", 434 | "dev": true 435 | }, 436 | "oauth-sign": { 437 | "version": "0.9.0", 438 | "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", 439 | "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", 440 | "dev": true 441 | }, 442 | "p-limit": { 443 | "version": "2.2.2", 444 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz", 445 | "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==", 446 | "dev": true, 447 | "requires": { 448 | "p-try": "^2.0.0" 449 | } 450 | }, 451 | "p-locate": { 452 | "version": "3.0.0", 453 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", 454 | "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", 455 | "dev": true, 456 | "requires": { 457 | "p-limit": "^2.0.0" 458 | } 459 | }, 460 | "p-try": { 461 | "version": "2.2.0", 462 | "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", 463 | "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", 464 | "dev": true 465 | }, 466 | "path-exists": { 467 | "version": "3.0.0", 468 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", 469 | "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", 470 | "dev": true 471 | }, 472 | "performance-now": { 473 | "version": "2.1.0", 474 | "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", 475 | "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", 476 | "dev": true 477 | }, 478 | "prettier": { 479 | "version": "1.19.1", 480 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", 481 | "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", 482 | "dev": true 483 | }, 484 | "prettier-plugin-apex": { 485 | "version": "1.1.0", 486 | "resolved": "https://registry.npmjs.org/prettier-plugin-apex/-/prettier-plugin-apex-1.1.0.tgz", 487 | "integrity": "sha512-7kNB78j2fRx9Kd2y5Rla6VLhzPCgaulrK+brRiwIGWWguEJAx43ggHA57tqQ73f/tRi9DOMQ25rTlYG04FWCOg==", 488 | "dev": true, 489 | "requires": { 490 | "axios": "^0.19.0", 491 | "jest-docblock": "^24.3.0", 492 | "wait-on": "^3.2.0", 493 | "yargs": "^14.0.0" 494 | } 495 | }, 496 | "psl": { 497 | "version": "1.7.0", 498 | "resolved": "https://registry.npmjs.org/psl/-/psl-1.7.0.tgz", 499 | "integrity": "sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ==", 500 | "dev": true 501 | }, 502 | "punycode": { 503 | "version": "2.1.1", 504 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 505 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", 506 | "dev": true 507 | }, 508 | "qs": { 509 | "version": "6.5.2", 510 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", 511 | "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", 512 | "dev": true 513 | }, 514 | "request": { 515 | "version": "2.88.0", 516 | "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", 517 | "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", 518 | "dev": true, 519 | "requires": { 520 | "aws-sign2": "~0.7.0", 521 | "aws4": "^1.8.0", 522 | "caseless": "~0.12.0", 523 | "combined-stream": "~1.0.6", 524 | "extend": "~3.0.2", 525 | "forever-agent": "~0.6.1", 526 | "form-data": "~2.3.2", 527 | "har-validator": "~5.1.0", 528 | "http-signature": "~1.2.0", 529 | "is-typedarray": "~1.0.0", 530 | "isstream": "~0.1.2", 531 | "json-stringify-safe": "~5.0.1", 532 | "mime-types": "~2.1.19", 533 | "oauth-sign": "~0.9.0", 534 | "performance-now": "^2.1.0", 535 | "qs": "~6.5.2", 536 | "safe-buffer": "^5.1.2", 537 | "tough-cookie": "~2.4.3", 538 | "tunnel-agent": "^0.6.0", 539 | "uuid": "^3.3.2" 540 | } 541 | }, 542 | "require-directory": { 543 | "version": "2.1.1", 544 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 545 | "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", 546 | "dev": true 547 | }, 548 | "require-main-filename": { 549 | "version": "2.0.0", 550 | "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", 551 | "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", 552 | "dev": true 553 | }, 554 | "rx": { 555 | "version": "4.1.0", 556 | "resolved": "https://registry.npmjs.org/rx/-/rx-4.1.0.tgz", 557 | "integrity": "sha1-pfE/957zt0D+MKqAP7CfmIBdR4I=", 558 | "dev": true 559 | }, 560 | "safe-buffer": { 561 | "version": "5.2.0", 562 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", 563 | "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==", 564 | "dev": true 565 | }, 566 | "safer-buffer": { 567 | "version": "2.1.2", 568 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 569 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", 570 | "dev": true 571 | }, 572 | "set-blocking": { 573 | "version": "2.0.0", 574 | "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", 575 | "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", 576 | "dev": true 577 | }, 578 | "sshpk": { 579 | "version": "1.16.1", 580 | "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", 581 | "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", 582 | "dev": true, 583 | "requires": { 584 | "asn1": "~0.2.3", 585 | "assert-plus": "^1.0.0", 586 | "bcrypt-pbkdf": "^1.0.0", 587 | "dashdash": "^1.12.0", 588 | "ecc-jsbn": "~0.1.1", 589 | "getpass": "^0.1.1", 590 | "jsbn": "~0.1.0", 591 | "safer-buffer": "^2.0.2", 592 | "tweetnacl": "~0.14.0" 593 | } 594 | }, 595 | "string-width": { 596 | "version": "3.1.0", 597 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", 598 | "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", 599 | "dev": true, 600 | "requires": { 601 | "emoji-regex": "^7.0.1", 602 | "is-fullwidth-code-point": "^2.0.0", 603 | "strip-ansi": "^5.1.0" 604 | } 605 | }, 606 | "strip-ansi": { 607 | "version": "5.2.0", 608 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", 609 | "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", 610 | "dev": true, 611 | "requires": { 612 | "ansi-regex": "^4.1.0" 613 | } 614 | }, 615 | "tough-cookie": { 616 | "version": "2.4.3", 617 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", 618 | "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", 619 | "dev": true, 620 | "requires": { 621 | "psl": "^1.1.24", 622 | "punycode": "^1.4.1" 623 | }, 624 | "dependencies": { 625 | "punycode": { 626 | "version": "1.4.1", 627 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", 628 | "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", 629 | "dev": true 630 | } 631 | } 632 | }, 633 | "tunnel-agent": { 634 | "version": "0.6.0", 635 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 636 | "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", 637 | "dev": true, 638 | "requires": { 639 | "safe-buffer": "^5.0.1" 640 | } 641 | }, 642 | "tweetnacl": { 643 | "version": "0.14.5", 644 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", 645 | "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", 646 | "dev": true 647 | }, 648 | "uri-js": { 649 | "version": "4.2.2", 650 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", 651 | "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", 652 | "dev": true, 653 | "requires": { 654 | "punycode": "^2.1.0" 655 | } 656 | }, 657 | "uuid": { 658 | "version": "3.4.0", 659 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", 660 | "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", 661 | "dev": true 662 | }, 663 | "verror": { 664 | "version": "1.10.0", 665 | "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", 666 | "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", 667 | "dev": true, 668 | "requires": { 669 | "assert-plus": "^1.0.0", 670 | "core-util-is": "1.0.2", 671 | "extsprintf": "^1.2.0" 672 | } 673 | }, 674 | "wait-on": { 675 | "version": "3.3.0", 676 | "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-3.3.0.tgz", 677 | "integrity": "sha512-97dEuUapx4+Y12aknWZn7D25kkjMk16PbWoYzpSdA8bYpVfS6hpl2a2pOWZ3c+Tyt3/i4/pglyZctG3J4V1hWQ==", 678 | "dev": true, 679 | "requires": { 680 | "@hapi/joi": "^15.0.3", 681 | "core-js": "^2.6.5", 682 | "minimist": "^1.2.0", 683 | "request": "^2.88.0", 684 | "rx": "^4.1.0" 685 | } 686 | }, 687 | "which-module": { 688 | "version": "2.0.0", 689 | "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", 690 | "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", 691 | "dev": true 692 | }, 693 | "wrap-ansi": { 694 | "version": "5.1.0", 695 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", 696 | "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", 697 | "dev": true, 698 | "requires": { 699 | "ansi-styles": "^3.2.0", 700 | "string-width": "^3.0.0", 701 | "strip-ansi": "^5.0.0" 702 | } 703 | }, 704 | "y18n": { 705 | "version": "4.0.0", 706 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", 707 | "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", 708 | "dev": true 709 | }, 710 | "yargs": { 711 | "version": "14.2.2", 712 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-14.2.2.tgz", 713 | "integrity": "sha512-/4ld+4VV5RnrynMhPZJ/ZpOCGSCeghMykZ3BhdFBDa9Wy/RH6uEGNWDJog+aUlq+9OM1CFTgtYRW5Is1Po9NOA==", 714 | "dev": true, 715 | "requires": { 716 | "cliui": "^5.0.0", 717 | "decamelize": "^1.2.0", 718 | "find-up": "^3.0.0", 719 | "get-caller-file": "^2.0.1", 720 | "require-directory": "^2.1.1", 721 | "require-main-filename": "^2.0.0", 722 | "set-blocking": "^2.0.0", 723 | "string-width": "^3.0.0", 724 | "which-module": "^2.0.0", 725 | "y18n": "^4.0.0", 726 | "yargs-parser": "^15.0.0" 727 | } 728 | }, 729 | "yargs-parser": { 730 | "version": "15.0.0", 731 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-15.0.0.tgz", 732 | "integrity": "sha512-xLTUnCMc4JhxrPEPUYD5IBR1mWCK/aT6+RJ/K29JY2y1vD+FhtgKK0AXRWvI262q3QSffAQuTouFIKUuHX89wQ==", 733 | "dev": true, 734 | "requires": { 735 | "camelcase": "^5.0.0", 736 | "decamelize": "^1.2.0" 737 | } 738 | } 739 | } 740 | } 741 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apex-rest-route", 3 | "version": "1.0.0", 4 | "description": "A simple library that allows the creation of RESTful API's.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/ChuckJonas/apex-rest-route.git" 12 | }, 13 | "author": "", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/ChuckJonas/apex-rest-route/issues" 17 | }, 18 | "homepage": "https://github.com/ChuckJonas/apex-rest-route#readme", 19 | "devDependencies": { 20 | "prettier": "^1.19.1", 21 | "prettier-plugin-apex": "^1.1.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /sfdx-project.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageDirectories": [ 3 | { 4 | "path": "force-app", 5 | "default": true, 6 | "package": "apex-rest-route", 7 | "versionName": "ver 0.1", 8 | "versionNumber": "0.1.0.NEXT" 9 | } 10 | ], 11 | "namespace": "", 12 | "sfdcLoginUrl": "https://login.salesforce.com", 13 | "sourceApiVersion": "47.0", 14 | "packageAliases": { 15 | "apex-rest-route": "0Ho1C000000002WSAQ", 16 | "apex-rest-route@0.1.0-1": "04t1C000000goM0QAI" 17 | } 18 | } --------------------------------------------------------------------------------