├── .cfconfig.json ├── .cfformat.json ├── .cflintrc ├── .editorconfig ├── .env.example ├── .gitattributes ├── .gitignore ├── Application.cfc ├── box.json ├── config ├── .htaccess ├── Application.cfc ├── CacheBox.cfc ├── Coldbox.cfc ├── Router.cfc └── WireBox.cfc ├── index.cfm ├── models └── .gitkeep ├── modules_app └── api │ ├── ModuleConfig.cfc │ ├── config │ └── Router.cfc │ ├── handlers │ └── BaseHandler.cfc │ ├── models │ └── Response.cfc │ └── modules_app │ ├── v1 │ ├── ModuleConfig.cfc │ ├── config │ │ └── Router.cfc │ ├── handlers │ │ ├── Echo.cfc │ │ └── Rants.cfc │ └── models │ │ ├── RantService.cfc │ │ └── UserService.cfc │ ├── v2 │ ├── ModuleConfig.cfc │ ├── config │ │ └── Router.cfc │ ├── handlers │ │ ├── Echo.cfc │ │ └── Rants.cfc │ └── models │ │ ├── RantService.cfc │ │ └── UserService.cfc │ ├── v3 │ ├── ModuleConfig.cfc │ ├── config │ │ └── Router.cfc │ ├── handlers │ │ ├── Echo.cfc │ │ └── Rants.cfc │ └── models │ │ ├── BaseService.cfc │ │ ├── RantService.cfc │ │ └── UserService.cfc │ ├── v4 │ ├── ModuleConfig.cfc │ ├── config │ │ └── Router.cfc │ ├── handlers │ │ ├── Echo.cfc │ │ └── Rants.cfc │ └── models │ │ ├── BaseService.cfc │ │ ├── Rant.cfc │ │ ├── RantService.cfc │ │ └── UserService.cfc │ ├── v5 │ ├── ModuleConfig.cfc │ ├── config │ │ └── Router.cfc │ ├── handlers │ │ ├── Echo.cfc │ │ └── Rants.cfc │ └── models │ │ ├── BaseEntity.cfc │ │ ├── BaseService.cfc │ │ ├── Rant.cfc │ │ ├── RantService.cfc │ │ └── UserService.cfc │ └── v6 │ ├── ModuleConfig.cfc │ ├── config │ └── Router.cfc │ ├── handlers │ ├── Echo.cfc │ └── Rants.cfc │ └── models │ ├── BaseEntity.cfc │ ├── BaseService.cfc │ ├── Rant.cfc │ ├── RantService.cfc │ └── UserService.cfc ├── readme.md ├── rest-readme.md ├── robots.txt ├── server.json ├── tests ├── Application.cfc ├── index.cfm ├── resources │ ├── .gitkeep │ └── BaseTest.cfc ├── runner.cfm ├── specs │ └── integration │ │ ├── api-v1 │ │ ├── EchoTests.cfc │ │ └── RantsTest.cfc │ │ ├── api-v2 │ │ └── RantsTest.cfc │ │ ├── api-v3 │ │ └── RantsTest.cfc │ │ ├── api-v4 │ │ └── RantsTest.cfc │ │ ├── api-v5 │ │ └── RantsTest.cfc │ │ └── api-v6 │ │ └── RantsTest.cfc └── test.xml └── workbench └── database └── 20200507_fluentapi.sql /.cfconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "requestTimeoutEnabled":true, 3 | "whitespaceManagement":"white-space-pref", 4 | "requestTimeout":"0,0,5,0", 5 | "cacheDefaultObject":"coldbox", 6 | "caches":{ 7 | "coldbox":{ 8 | "storage":"true", 9 | "type":"RAM", 10 | "custom":{ 11 | "timeToIdleSeconds":"1800", 12 | "timeToLiveSeconds":"3600" 13 | }, 14 | "class":"lucee.runtime.cache.ram.RamCache", 15 | "readOnly":"false" 16 | } 17 | }, 18 | "datasources" : { 19 | "${DB_DATABASE}":{ 20 | "host":"${DB_HOST}", 21 | "dbdriver":"${DB_DRIVER}", 22 | "database":"${DB_DATABASE}", 23 | "dsn":"jdbc:mysql://{host}:{port}/{database}", 24 | "custom":"useUnicode=true&characterEncoding=UTF-8&useLegacyDatetimeCode=true&autoReconnect=true", 25 | "port":"${DB_PORT}", 26 | "class":"${DB_CLASS}", 27 | "username":"${DB_USER}", 28 | "password":"${DB_PASSWORD}", 29 | "connectionLimit":"100", 30 | "connectionTimeout":"1" 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /.cfformat.json: -------------------------------------------------------------------------------- 1 | { 2 | "alignment.consecutive.assignments":false, 3 | "alignment.consecutive.params":false, 4 | "alignment.consecutive.properties":false, 5 | "array.empty_padding":false, 6 | "array.multiline.element_count":4, 7 | "array.multiline.leading_comma":false, 8 | "array.multiline.leading_comma.padding":true, 9 | "array.multiline.min_length":40, 10 | "array.padding":true, 11 | "binary_operators.padding":true, 12 | "brackets.padding":true, 13 | "comment.asterisks":"align", 14 | "for_loop_semicolons.padding":true, 15 | "function_anonymous.empty_padding":false, 16 | "function_anonymous.group_to_block_spacing":"spaced", 17 | "function_anonymous.multiline.element_count":4, 18 | "function_anonymous.multiline.leading_comma":false, 19 | "function_anonymous.multiline.leading_comma.padding":true, 20 | "function_anonymous.multiline.min_length":40, 21 | "function_anonymous.padding":true, 22 | "function_call.casing.builtin":"cfdocs", 23 | "function_call.casing.userdefined":"camel", 24 | "function_call.empty_padding":false, 25 | "function_call.multiline.element_count":4, 26 | "function_call.multiline.leading_comma":false, 27 | "function_call.multiline.leading_comma.padding":true, 28 | "function_call.multiline.min_length":40, 29 | "function_call.padding":true, 30 | "function_declaration.empty_padding":false, 31 | "function_declaration.group_to_block_spacing":"spaced", 32 | "function_declaration.multiline.element_count":4, 33 | "function_declaration.multiline.leading_comma":false, 34 | "function_declaration.multiline.leading_comma.padding":true, 35 | "function_declaration.multiline.min_length":40, 36 | "function_declaration.padding":true, 37 | "indent_size":4, 38 | "keywords.block_to_keyword_spacing":"spaced", 39 | "keywords.empty_group_spacing":false, 40 | "keywords.group_to_block_spacing":"spaced", 41 | "keywords.padding_inside_group":true, 42 | "keywords.spacing_to_block":"spaced", 43 | "keywords.spacing_to_group":true, 44 | "max_columns":120, 45 | "method_call.chain.multiline":3, 46 | "newline":"\n", 47 | "parentheses.padding":true, 48 | "strings.attributes.quote":"double", 49 | "strings.quote":"double", 50 | "struct.empty_padding":false, 51 | "struct.multiline.element_count":4, 52 | "struct.multiline.leading_comma":false, 53 | "struct.multiline.leading_comma.padding":true, 54 | "struct.multiline.min_length":40, 55 | "struct.padding":true, 56 | "struct.separator":": ", 57 | "tab_indent":true 58 | } 59 | -------------------------------------------------------------------------------- /.cflintrc: -------------------------------------------------------------------------------- 1 | { 2 | "output": [], 3 | "rule": [], 4 | "includes": [ 5 | { 6 | "code": "AVOID_USING_STRUCTNEW" 7 | }, 8 | { 9 | "code": "AVOID_USING_ARRAYNEW" 10 | }, 11 | { 12 | "code": "AVOID_USING_CFINCLUDE_TAG" 13 | }, 14 | { 15 | "code": "AVOID_USING_CFABORT_TAG" 16 | }, 17 | { 18 | "code": "AVOID_USING_ABORT" 19 | }, 20 | { 21 | "code": "ARG_VAR_CONFLICT" 22 | }, 23 | { 24 | "code": "ARG_VAR_MIXED" 25 | }, 26 | { 27 | "code": "CFQUERYPARAM_REQ" 28 | }, 29 | { 30 | "code": "COMPARE_INSTEAD_OF_ASSIGN" 31 | }, 32 | { 33 | "code": "COMPONENT_HINT_MISSING" 34 | }, 35 | { 36 | "code": "EXCESSIVE_FUNCTION_LENGTH" 37 | }, 38 | { 39 | "code": "EXCESSIVE_COMPONENT_LENGTH" 40 | }, 41 | { 42 | "code": "EXCESSIVE_ARGUMENTS" 43 | }, 44 | { 45 | "code": "EXCESSIVE_FUNCTIONS" 46 | }, 47 | { 48 | "code": "FUNCTION_TOO_COMPLEX" 49 | }, 50 | { 51 | "code": "FUNCTION_HINT_MISSING" 52 | }, 53 | { 54 | "code": "LOCAL_LITERAL_VALUE_USED_TOO_OFTEN" 55 | }, 56 | { 57 | "code": "GLOBAL_LITERAL_VALUE_USED_TOO_OFTEN" 58 | }, 59 | { 60 | "code": "MISSING_VAR" 61 | }, 62 | { 63 | "code": "OUTPUT_ATTR" 64 | }, 65 | { 66 | "code": "UNUSED_LOCAL_VARIABLE" 67 | }, 68 | { 69 | "code": "UNUSED_METHOD_ARGUMENT" 70 | }, 71 | { 72 | "code": "SQL_SELECT_STAR" 73 | }, 74 | { 75 | "code": "VAR_ALLCAPS_NAME" 76 | } 77 | ], 78 | "inheritParent": false, 79 | "inheritPlugins": true, 80 | "parameters": {} 81 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = false 10 | indent_style = tab 11 | indent_size = 4 12 | tab_width = 4 13 | 14 | [*.yml] 15 | indent_style = space 16 | indent_size = 2 17 | 18 | [*.{md,markdown}] 19 | trim_trailing_whitespace = false 20 | insert_final_newline = false -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # ColdBox Environment 2 | APPNAME=fluentAPI 3 | ENVIRONMENT=development 4 | 5 | # Database Information 6 | DB_CLASS=com.mysql.jdbc.Driver 7 | DB_DRIVER=MySQL 8 | DB_HOST=127.0.0.1 9 | DB_PORT=3306 10 | DB_DATABASE=coldbox 11 | DB_USER=root 12 | DB_PASSWORD= 13 | 14 | # JWT Information 15 | JWT_SECRET= 16 | 17 | # S3 Information 18 | S3_ACCESS_KEY= 19 | S3_SECRET_KEY= 20 | S3_REGION=us-east-1 21 | S3_DOMAIN=amazonaws.com 22 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # General Ignores + IDE 2 | .DS_Store 3 | settings.xml 4 | WEB-INF 5 | .env 6 | 7 | # logs + tests 8 | logs/** 9 | tests/results/** 10 | 11 | # npm 12 | **/node_modules/* 13 | npm-debug.log 14 | yarn-error.log 15 | 16 | ## Ignored Dependencies 17 | coldbox/* 18 | testbox/* 19 | modules/* 20 | **/apidocs/** -------------------------------------------------------------------------------- /Application.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2005-2007 ColdBox Framework by Luis Majano and Ortus Solutions, Corp 3 | * www.ortussolutions.com 4 | * --- 5 | */ 6 | component{ 7 | // Application properties 8 | this.name = hash( getCurrentTemplatePath() ); 9 | this.sessionManagement = true; 10 | this.sessionTimeout = createTimeSpan(0,0,30,0); 11 | this.setClientCookies = true; 12 | this.datasource = "fluentAPI"; 13 | // COLDBOX STATIC PROPERTY, DO NOT CHANGE UNLESS THIS IS NOT THE ROOT OF YOUR COLDBOX APP 14 | COLDBOX_APP_ROOT_PATH = getDirectoryFromPath( getCurrentTemplatePath() ); 15 | // The web server mapping to this application. Used for remote purposes or static purposes 16 | COLDBOX_APP_MAPPING = ""; 17 | // COLDBOX PROPERTIES 18 | COLDBOX_CONFIG_FILE = ""; 19 | // COLDBOX APPLICATION KEY OVERRIDE 20 | COLDBOX_APP_KEY = ""; 21 | 22 | // application start 23 | public boolean function onApplicationStart(){ 24 | application.cbBootstrap = new coldbox.system.Bootstrap( COLDBOX_CONFIG_FILE, COLDBOX_APP_ROOT_PATH, COLDBOX_APP_KEY, COLDBOX_APP_MAPPING ); 25 | application.cbBootstrap.loadColdbox(); 26 | return true; 27 | } 28 | 29 | // application end 30 | public boolean function onApplicationEnd( struct appScope ){ 31 | arguments.appScope.cbBootstrap.onApplicationEnd( arguments.appScope ); 32 | } 33 | 34 | // request start 35 | public boolean function onRequestStart( string targetPage ){ 36 | // Process ColdBox Request 37 | application.cbBootstrap.onRequestStart( arguments.targetPage ); 38 | 39 | return true; 40 | } 41 | 42 | public void function onSessionStart(){ 43 | application.cbBootStrap.onSessionStart(); 44 | } 45 | 46 | public void function onSessionEnd( struct sessionScope, struct appScope ){ 47 | arguments.appScope.cbBootStrap.onSessionEnd( argumentCollection=arguments ); 48 | } 49 | 50 | public boolean function onMissingTemplate( template ){ 51 | return application.cbBootstrap.onMissingTemplate( argumentCollection=arguments ); 52 | } 53 | 54 | } -------------------------------------------------------------------------------- /box.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"FluentAPI", 3 | "version":"1.0.0", 4 | "location":"", 5 | "author":"You", 6 | "slug":"fluentapi", 7 | "createPackageDirectory":false, 8 | "type":"mvc", 9 | "homepage":"https://github.com/coldbox-templates/rest-hmvc", 10 | "documentation":"https://github.com/coldbox-templates/rest-hmvc", 11 | "repository":{ 12 | "type":"git", 13 | "url":"https://github.com/coldbox-templates/rest-hmvc" 14 | }, 15 | "bugs":"https://github.com/coldbox-templates/rest-hmvc/issues", 16 | "shortDescription":"A modular API RESTFul template", 17 | "contributors":[], 18 | "ignore":[], 19 | "devDependencies":{ 20 | "relax":"^4.1.0+174", 21 | "testbox":"^3.0.0", 22 | "commandbox-dotenv":"*", 23 | "commandbox-cfconfig":"*", 24 | "commandbox-cfformat":"*" 25 | }, 26 | "installPaths":{ 27 | "coldbox":"coldbox/", 28 | "relax":"modules/relax/", 29 | "testbox":"testbox/", 30 | "cbSwagger":"modules/cbSwagger/", 31 | "cbvalidation":"modules/cbvalidation/", 32 | "route-visualizer":"modules/route-visualizer/", 33 | "cors":"modules/cors/", 34 | "mementifier":"modules/mementifier/" 35 | }, 36 | "dependencies":{ 37 | "coldbox":"5.6.2", 38 | "cbswagger":"^2.1.1+118", 39 | "cbvalidation":"^2.1.0+126", 40 | "route-visualizer":"^1.4.0+24", 41 | "cors":"^3.0.3", 42 | "mementifier":"^2.1.0+100" 43 | }, 44 | "scripts":{ 45 | "lint":"cflint **.cf* --text --html --json --!exitOnError --suppress", 46 | "format":"cfformat run modules_app/**/*.cfc,tests/specs/**/*.cfc --overwrite --verbose", 47 | "format:check":"cfformat check modules_app/**/*.cfc,tests/specs/**/*.cfc --verbose" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /config/.htaccess: -------------------------------------------------------------------------------- 1 | #apache access file to protect the config.xml.cfm file. Delete this if you do not use apache. 2 | authtype Basic 3 | deny from all 4 | Options -Indexes -------------------------------------------------------------------------------- /config/Application.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a protection Application cfm for the config file. You do not 3 | * need to modify this file 4 | */ 5 | component{ 6 | abort; 7 | } -------------------------------------------------------------------------------- /config/CacheBox.cfc: -------------------------------------------------------------------------------- 1 | component{ 2 | 3 | /** 4 | * Configure CacheBox for ColdBox Application Operation 5 | */ 6 | function configure(){ 7 | 8 | // The CacheBox configuration structure DSL 9 | cacheBox = { 10 | // LogBox config already in coldbox app, not needed 11 | // logBoxConfig = "coldbox.system.web.config.LogBox", 12 | 13 | // The defaultCache has an implicit name "default" which is a reserved cache name 14 | // It also has a default provider of cachebox which cannot be changed. 15 | // All timeouts are in minutes 16 | defaultCache = { 17 | objectDefaultTimeout = 120, //two hours default 18 | objectDefaultLastAccessTimeout = 30, //30 minutes idle time 19 | useLastAccessTimeouts = true, 20 | reapFrequency = 5, 21 | freeMemoryPercentageThreshold = 0, 22 | evictionPolicy = "LRU", 23 | evictCount = 1, 24 | maxObjects = 300, 25 | objectStore = "ConcurrentStore", //guaranteed objects 26 | coldboxEnabled = true 27 | }, 28 | 29 | // Register all the custom named caches you like here 30 | caches = { 31 | // Named cache for all coldbox event and view template caching 32 | template = { 33 | provider = "coldbox.system.cache.providers.CacheBoxColdBoxProvider", 34 | properties = { 35 | objectDefaultTimeout = 120, 36 | objectDefaultLastAccessTimeout = 30, 37 | useLastAccessTimeouts = true, 38 | freeMemoryPercentageThreshold = 0, 39 | reapFrequency = 5, 40 | evictionPolicy = "LRU", 41 | evictCount = 2, 42 | maxObjects = 300, 43 | objectStore = "ConcurrentSoftReferenceStore" //memory sensitive 44 | } 45 | } 46 | } 47 | }; 48 | } 49 | 50 | } -------------------------------------------------------------------------------- /config/Coldbox.cfc: -------------------------------------------------------------------------------- 1 | component{ 2 | 3 | // Configure ColdBox Application 4 | function configure(){ 5 | 6 | // coldbox directives 7 | coldbox = { 8 | //Application Setup 9 | appName = getSystemSetting( "APPNAME", "Your app name here" ), 10 | eventName = "event", 11 | 12 | //Development Settings 13 | reinitPassword = "", 14 | handlersIndexAutoReload = true, 15 | 16 | //Implicit Events 17 | defaultEvent = "v1:echo.index", 18 | requestStartHandler = "", 19 | requestEndHandler = "", 20 | applicationStartHandler = "", 21 | applicationEndHandler = "", 22 | sessionStartHandler = "", 23 | sessionEndHandler = "", 24 | missingTemplateHandler = "", 25 | 26 | //Extension Points 27 | applicationHelper = "", 28 | viewsHelper = "", 29 | modulesExternalLocation = [], 30 | viewsExternalLocation = "", 31 | layoutsExternalLocation = "", 32 | handlersExternalLocation = "", 33 | requestContextDecorator = "", 34 | controllerDecorator = "", 35 | 36 | //Error/Exception Handling 37 | invalidHTTPMethodHandler = "", 38 | exceptionHandler = "", 39 | invalidEventHandler = "", 40 | customErrorTemplate = "", 41 | 42 | //Application Aspects 43 | handlerCaching = false, 44 | eventCaching = false, 45 | viewCaching = false, 46 | // Will automatically do a mapDirectory() on your `models` for you. 47 | autoMapModels = true, 48 | jsonPayloadToRC = true 49 | }; 50 | 51 | // custom settings 52 | settings = { 53 | 54 | }; 55 | 56 | // environment settings, create a detectEnvironment() method to detect it yourself. 57 | // create a function with the name of the environment so it can be executed if that environment is detected 58 | // the value of the environment is a list of regex patterns to match the cgi.http_host. 59 | environments = { 60 | development = "localhost,127\.0\.0\.1" 61 | }; 62 | 63 | // Module Directives 64 | modules = { 65 | // An array of modules names to load, empty means all of them 66 | include = [], 67 | // An array of modules names to NOT load, empty means none 68 | exclude = [] 69 | }; 70 | 71 | //LogBox DSL 72 | logBox = { 73 | // Define Appenders 74 | appenders = { 75 | coldboxTracer = { class="coldbox.system.logging.appenders.ConsoleAppender" } 76 | }, 77 | // Root Logger 78 | root = { levelmax="INFO", appenders="*" }, 79 | // Implicit Level Categories 80 | info = [ "coldbox.system" ] 81 | }; 82 | 83 | //Layout Settings 84 | layoutSettings = { 85 | defaultLayout = "", 86 | defaultView = "" 87 | }; 88 | 89 | //Interceptor Settings 90 | interceptorSettings = { 91 | customInterceptionPoints = "" 92 | }; 93 | 94 | //Register interceptors as an array, we need order 95 | interceptors = [ 96 | ]; 97 | 98 | // module setting overrides 99 | moduleSettings = { 100 | cbswagger = { 101 | // The route prefix to search. Routes beginning with this prefix will be determined to be api routes 102 | "routes" : [ "api" ], 103 | // The default output format: json or yml 104 | // Any routes to exclude 105 | "excludeRoutes" : [], 106 | "defaultFormat" : "json", 107 | // A convention route, relative to your app root, where request/response samples are stored ( e.g. resources/apidocs/responses/[module].[handler].[action].[HTTP Status Code].json ) 108 | "samplesPath" : "resources/apidocs", 109 | // Information about your API 110 | "info" :{ 111 | // A title for your API 112 | "title" : "Fluent API for SoapBox Twitter clone", 113 | // A description of your API 114 | "description" : "This Fluent API provides data the for SoapBox Twitter clone", 115 | // A terms of service URL for your API 116 | "termsOfService" : "", 117 | //The contact email address 118 | "contact" :{ 119 | "name": "Gavin Pickin", 120 | "url": "https://www.ortussolutions.com", 121 | "email": "gavin@ortussolutions.com" 122 | }, 123 | //A url to the License of your API 124 | "license": { 125 | "name": "Apache 2.0", 126 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 127 | }, 128 | //The version of your API 129 | "version":"1.0.0" 130 | }, 131 | 132 | // https://swagger.io/specification/#serverObject 133 | "servers" : [ 134 | { 135 | "url" : "http://127.0.0.1:60146/", 136 | "description" : "Local Development" 137 | } 138 | ], 139 | 140 | // An element to hold various schemas for the specification. 141 | // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#componentsObject 142 | "components" : { 143 | 144 | // Define your security schemes here 145 | // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#securitySchemeObject 146 | "securitySchemes" : {} 147 | } 148 | } 149 | }; 150 | 151 | /* 152 | 153 | // flash scope configuration 154 | flash = { 155 | scope = "session,client,cluster,ColdboxCache,or full path", 156 | properties = {}, // constructor properties for the flash scope implementation 157 | inflateToRC = true, // automatically inflate flash data into the RC scope 158 | inflateToPRC = false, // automatically inflate flash data into the PRC scope 159 | autoPurge = true, // automatically purge flash data for you 160 | autoSave = true // automatically save flash scopes at end of a request and on relocations. 161 | }; 162 | 163 | //Register Layouts 164 | layouts = [ 165 | { name = "login", 166 | file = "Layout.tester.cfm", 167 | views = "vwLogin,test", 168 | folders = "tags,pdf/single" 169 | } 170 | ]; 171 | 172 | //Conventions 173 | conventions = { 174 | handlersLocation = "handlers", 175 | viewsLocation = "views", 176 | layoutsLocation = "layouts", 177 | modelsLocation = "models", 178 | eventAction = "index" 179 | }; 180 | 181 | */ 182 | 183 | } 184 | 185 | /** 186 | * Development environment 187 | */ 188 | function development(){ 189 | coldbox.customErrorTemplate = "/coldbox/system/includes/BugReport.cfm"; 190 | } 191 | 192 | } -------------------------------------------------------------------------------- /config/Router.cfc: -------------------------------------------------------------------------------- 1 | component{ 2 | 3 | function configure(){ 4 | // Set Full Rewrites 5 | setFullRewrites( true ); 6 | 7 | /** 8 | * -------------------------------------------------------------------------- 9 | * App Routes 10 | * -------------------------------------------------------------------------- 11 | * 12 | * Here is where you can register the routes for your web application! 13 | * Go get Funky! 14 | * 15 | */ 16 | 17 | // A nice healthcheck route example 18 | route("/healthcheck",function(event,rc,prc){ 19 | return "Ok!"; 20 | }); 21 | 22 | // Conventions based routing 23 | route( ":handler/:action?" ).end(); 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /config/WireBox.cfc: -------------------------------------------------------------------------------- 1 | component extends="coldbox.system.ioc.config.Binder"{ 2 | 3 | /** 4 | * Configure WireBox, that's it! 5 | */ 6 | function configure(){ 7 | 8 | // The WireBox configuration structure DSL 9 | wireBox = { 10 | // Scope registration, automatically register a wirebox injector instance on any CF scope 11 | // By default it registeres itself on application scope 12 | scopeRegistration = { 13 | enabled = true, 14 | scope = "application", // server, cluster, session, application 15 | key = "wireBox" 16 | }, 17 | 18 | // DSL Namespace registrations 19 | customDSL = { 20 | // namespace = "mapping name" 21 | }, 22 | 23 | // Custom Storage Scopes 24 | customScopes = { 25 | // annotationName = "mapping name" 26 | }, 27 | 28 | // Package scan locations 29 | scanLocations = [], 30 | 31 | // Stop Recursions 32 | stopRecursions = [], 33 | 34 | // Parent Injector to assign to the configured injector, this must be an object reference 35 | parentInjector = "", 36 | 37 | // Register all event listeners here, they are created in the specified order 38 | listeners = [ 39 | // { class="", name="", properties={} } 40 | ] 41 | }; 42 | 43 | // Map Bindings below 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /index.cfm: -------------------------------------------------------------------------------- 1 |  2 | 9 | 10 | -------------------------------------------------------------------------------- /models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gpickin/itb2020-fluentAPI/f005189807e2671da34f4be8383043c7d174171e/models/.gitkeep -------------------------------------------------------------------------------- /modules_app/api/ModuleConfig.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | Module Directives as public properties 3 | this.title = "Title of the module"; 4 | this.author = "Author of the module"; 5 | this.webURL = "Web URL for docs purposes"; 6 | this.description = "Module description"; 7 | this.version = "Module Version"; 8 | this.viewParentLookup = (true) [boolean] (Optional) // If true, checks for views in the parent first, then it the module.If false, then modules first, then parent. 9 | this.layoutParentLookup = (true) [boolean] (Optional) // If true, checks for layouts in the parent first, then it the module.If false, then modules first, then parent. 10 | this.entryPoint = "" (Optional) // If set, this is the default event (ex:forgebox:manager.index) or default route (/forgebox) the framework 11 | will use to create an entry link to the module. Similar to a default event. 12 | this.cfmapping = "The CF mapping to create"; 13 | this.modelNamespace = "The namespace to use for registered models, if blank it uses the name of the module." 14 | this.dependencies = "The array of dependencies for this module" 15 | 16 | structures to create for configuration 17 | - parentSettings : struct (will append and override parent) 18 | - settings : struct 19 | - interceptorSettings : struct of the following keys ATM 20 | - customInterceptionPoints : string list of custom interception points 21 | - interceptors : array 22 | - layoutSettings : struct (will allow to define a defaultLayout for the module) 23 | - routes : array Allowed keys are same as the addRoute() method of the SES interceptor. 24 | - wirebox : The wirebox DSL to load and use 25 | 26 | Available objects in variable scope 27 | - controller 28 | - appMapping (application mapping) 29 | - moduleMapping (include,cf path) 30 | - modulePath (absolute path) 31 | - log (A pre-configured logBox logger object for this object) 32 | - binder (The wirebox configuration binder) 33 | - wirebox (The wirebox injector) 34 | 35 | Required Methods 36 | - configure() : The method ColdBox calls to configure the module. 37 | 38 | Optional Methods 39 | - onLoad() : If found, it is fired once the module is fully loaded 40 | - onUnload() : If found, it is fired once the module is unloaded 41 | 42 | */ 43 | component { 44 | 45 | // Module Properties 46 | this.title = "api"; 47 | this.author = ""; 48 | this.webURL = ""; 49 | this.description = ""; 50 | this.version = "1.0.0"; 51 | // If true, looks for views in the parent first, if not found, then in the module. Else vice-versa 52 | this.viewParentLookup = true; 53 | // If true, looks for layouts in the parent first, if not found, then in module. Else vice-versa 54 | this.layoutParentLookup = true; 55 | // Module Entry Point 56 | this.entryPoint = "api"; 57 | // Inheritable entry point. 58 | this.inheritEntryPoint = true; 59 | // Model Namespace 60 | this.modelNamespace = "api"; 61 | // CF Mapping 62 | this.cfmapping = "api"; 63 | // Auto-map models 64 | this.autoMapModels = true; 65 | // Module Dependencies 66 | this.dependencies = []; 67 | 68 | function configure() { 69 | // parent settings 70 | parentSettings = {}; 71 | 72 | // module settings - stored in modules.name.settings 73 | settings = {}; 74 | 75 | // Layout Settings 76 | layoutSettings = { defaultLayout: "" }; 77 | 78 | // Custom Declared Points 79 | interceptorSettings = { customInterceptionPoints: "" }; 80 | 81 | // Custom Declared Interceptors 82 | interceptors = []; 83 | } 84 | 85 | /** 86 | * Fired when the module is registered and activated. 87 | */ 88 | function onLoad() { 89 | } 90 | 91 | /** 92 | * Fired when the module is unregistered and unloaded 93 | */ 94 | function onUnload() { 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /modules_app/api/config/Router.cfc: -------------------------------------------------------------------------------- 1 | component { 2 | 3 | function configure() { 4 | } 5 | 6 | } 7 | -------------------------------------------------------------------------------- /modules_app/api/handlers/BaseHandler.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * ******************************************************************************** 3 | * Copyright 2005-2007 ColdBox Framework by Luis Majano and Ortus Solutions, Corp 4 | * www.ortussolutions.com 5 | * ******************************************************************************** 6 | * Base RESTFul handler spice up as needed. 7 | * This handler will create a Response model and prepare it for your actions to use 8 | * to produce RESTFul responses. 9 | */ 10 | component extends="coldbox.system.EventHandler" { 11 | 12 | // Pseudo "constants" used in API Response/Method parsing 13 | property name="METHODS"; 14 | property name="STATUS"; 15 | 16 | // Verb aliases - in case we are dealing with legacy browsers or servers ( e.g. IIS7 default ) 17 | METHODS = { 18 | "HEAD": "HEAD", 19 | "GET": "GET", 20 | "POST": "POST", 21 | "PATCH": "PATCH", 22 | "PUT": "PUT", 23 | "DELETE": "DELETE" 24 | }; 25 | 26 | // HTTP STATUS CODES 27 | STATUS = { 28 | "CREATED": 201, 29 | "ACCEPTED": 202, 30 | "SUCCESS": 200, 31 | "NO_CONTENT": 204, 32 | "RESET": 205, 33 | "PARTIAL_CONTENT": 206, 34 | "BAD_REQUEST": 400, 35 | "NOT_AUTHORIZED": 403, 36 | "NOT_AUTHENTICATED": 401, 37 | "NOT_FOUND": 404, 38 | "NOT_ALLOWED": 405, 39 | "NOT_ACCEPTABLE": 406, 40 | "TOO_MANY_REQUESTS": 429, 41 | "EXPECTATION_FAILED": 417, 42 | "INTERNAL_ERROR": 500, 43 | "NOT_IMPLEMENTED": 501 44 | }; 45 | 46 | // OPTIONAL HANDLER PROPERTIES 47 | this.prehandler_only = ""; 48 | this.prehandler_except = ""; 49 | this.posthandler_only = ""; 50 | this.posthandler_except = ""; 51 | this.aroundHandler_only = ""; 52 | this.aroundHandler_except = ""; 53 | 54 | // REST Allowed HTTP Methods Ex: this.allowedMethods = {delete='#METHODS.POST#,#METHODS.DELETE#',index='#METTHOD.GET#'} 55 | this.allowedMethods = { 56 | "index": METHODS.GET, 57 | "get": METHODS.GET, 58 | "create": METHODS.POST, 59 | "list": METHODS.GET, 60 | "update": METHODS.PUT & "," & METHODS.PATCH, 61 | "delete": METHODS.DELETE 62 | }; 63 | 64 | /** 65 | * Around handler for all actions it inherits 66 | */ 67 | function aroundHandler( 68 | event, 69 | rc, 70 | prc, 71 | targetAction, 72 | eventArguments 73 | ) { 74 | try { 75 | // start a resource timer 76 | var stime = getTickCount(); 77 | // prepare our response object 78 | prc.response = getModel( "Response@api" ); 79 | // prepare argument execution 80 | var args = { event: arguments.event, rc: arguments.rc, prc: arguments.prc }; 81 | structAppend( args, arguments.eventArguments ); 82 | // Incoming Format Detection 83 | if ( !isNull( rc.format ) ) { 84 | prc.response.setFormat( rc.format ); 85 | } 86 | // Execute action 87 | var actionResults = arguments.targetAction( argumentCollection = args ); 88 | } 89 | // Auth Issues 90 | catch ( "InvalidCredentials" e ) { 91 | this.onAuthenticationFailure( argumentCollection = arguments ); 92 | } 93 | // Validation Exceptions 94 | catch ( "ValidationException" e ) { 95 | arguments.exception = e; 96 | this.onValidationException( argumentCollection = arguments ); 97 | } catch ( "EntityNotFound" e ) { 98 | prc.response.setErrorMessage( e.message, 404 ); 99 | 100 | if ( listFindNoCase( "development,staging", getSetting( "environment" ) ) ) { 101 | prc.response 102 | .addMessage( "Detail: #e.detail#" ) 103 | .addMessage( "StackTrace: #e.stacktrace#" ); 104 | } 105 | // Render Error Out 106 | event.renderData( 107 | type = prc.response.getFormat(), 108 | data = prc.response.getDataPacket(), 109 | contentType = prc.response.getContentType(), 110 | statusCode = prc.response.getStatusCode(), 111 | statusText = prc.response.getStatusText(), 112 | location = prc.response.getLocation(), 113 | isBinary = prc.response.getBinary() 114 | ); 115 | } catch ( Any e ) { 116 | // Log Locally 117 | log.error( 118 | "Error calling #event.getCurrentEvent()#: #e.message# #e.detail#", 119 | { "_stacktrace": e.stacktrace, "httpData": getHTTPRequestData() } 120 | ); 121 | // Setup General Error Response 122 | prc.response 123 | .setError( true ) 124 | .addMessage( "General application error: #e.message#" ) 125 | .setStatusCode( STATUS.INTERNAL_ERROR ) 126 | .setStatusText( "General application error" ); 127 | 128 | // Development additions 129 | if ( getSetting( "environment" ) eq "development" ) { 130 | prc.response 131 | .addMessage( "Detail: #e.detail#" ) 132 | .addMessage( "StackTrace: #e.stacktrace#" ); 133 | } 134 | } 135 | 136 | // Development additions 137 | if ( getSetting( "environment" ) eq "development" ) { 138 | prc.response 139 | .addHeader( "x-current-route", event.getCurrentRoute() ) 140 | .addHeader( "x-current-routed-url", event.getCurrentRoutedURL() ) 141 | .addHeader( "x-current-routed-namespace", event.getCurrentRoutedNamespace() ) 142 | .addHeader( "x-current-event", event.getCurrentEvent() ); 143 | } 144 | // end timer 145 | prc.response.setResponseTime( getTickCount() - stime ); 146 | 147 | // Did the controllers set a view to be rendered? If not use renderdata, else just delegate to view. 148 | if ( 149 | isNull( actionResults ) 150 | AND 151 | !event.getCurrentView().len() 152 | AND 153 | event.getRenderData().isEmpty() 154 | ) { 155 | // Get response data 156 | var responseData = prc.response.getDataPacket(); 157 | // If we have an error flag, render our messages and omit any marshalled data 158 | if ( prc.response.getError() ) { 159 | responseData = prc.response.getDataPacket(); 160 | } 161 | // Magical renderings 162 | event.renderData( 163 | type = prc.response.getFormat(), 164 | data = responseData, 165 | contentType = prc.response.getContentType(), 166 | statusCode = prc.response.getStatusCode(), 167 | statusText = prc.response.getStatusText(), 168 | location = prc.response.getLocation(), 169 | isBinary = prc.response.getBinary(), 170 | jsonCallback = prc.response.getJsonCallback(), 171 | jsonQueryFormat = prc.response.getJsonQueryFormat() 172 | ); 173 | } 174 | 175 | // Global Response Headers 176 | prc.response 177 | .addHeader( "x-response-time", prc.response.getResponseTime() ) 178 | .addHeader( "x-cached-response", prc.response.getCachedResponse() ); 179 | 180 | // Response Headers 181 | for ( var thisHeader in prc.response.getHeaders() ) { 182 | event.setHTTPHeader( name = thisHeader.name, value = thisHeader.value ); 183 | } 184 | 185 | // If results detected, just return them, controllers requesting to return results 186 | if ( !isNull( actionResults ) ) { 187 | return actionResults; 188 | } 189 | } 190 | 191 | /** 192 | * on localized errors 193 | */ 194 | function onError( 195 | event, 196 | rc, 197 | prc, 198 | faultAction, 199 | exception, 200 | eventArguments 201 | ) { 202 | // Log Locally 203 | log.error( 204 | "Error in base handler (#arguments.faultAction#): #arguments.exception.message# #arguments.exception.detail#", 205 | { "_stacktrace": arguments.exception.stacktrace, "httpData": getHTTPRequestData() } 206 | ); 207 | 208 | // Verify response exists, else create one 209 | if ( !structKeyExists( prc, "Response" ) ) { 210 | prc.response = getModel( "Response@api" ); 211 | } 212 | 213 | // Setup General Error Response 214 | prc.response 215 | .setError( true ) 216 | .addMessage( "Base Handler Application Error: #arguments.exception.message#" ) 217 | .setStatusCode( STATUS.INTERNAL_ERROR ) 218 | .setStatusText( "General application error" ); 219 | 220 | // Development additions 221 | if ( getSetting( "environment" ) eq "development" ) { 222 | prc.response 223 | .addMessage( "Detail: #arguments.exception.detail#" ) 224 | .addMessage( "StackTrace: #arguments.exception.stacktrace#" ); 225 | } 226 | 227 | // If in development, then it will show full trace error template, else render data 228 | if ( getSetting( "environment" ) neq "development2" ) { 229 | // Render Error Out 230 | event.renderData( 231 | type = prc.response.getFormat(), 232 | data = prc.response.getDataPacket(), 233 | contentType = prc.response.getContentType(), 234 | statusCode = prc.response.getStatusCode(), 235 | statusText = prc.response.getStatusText(), 236 | location = prc.response.getLocation(), 237 | isBinary = prc.response.getBinary() 238 | ); 239 | } 240 | } 241 | 242 | /** 243 | * on validation errors via cbValidation 244 | */ 245 | function onValidationException( 246 | event, 247 | rc, 248 | prc, 249 | eventArguments, 250 | exception 251 | ) { 252 | // Log Locally 253 | if ( log.canDebug() ) { 254 | log.debug( 255 | "ValidationException Execution of (#arguments.event.getCurrentEvent()#)", 256 | arguments.exception.extendedInfo 257 | ); 258 | } 259 | 260 | var allErrors = deserializeJSON( exception.extendedInfo ).reduce( function( allErrors, field, fieldErrors ) { 261 | arrayAppend( 262 | allErrors, 263 | fieldErrors.map( function( error ) { 264 | return error.message; 265 | } ), 266 | true 267 | ); 268 | return allErrors; 269 | }, [] ); 270 | prc.response.setErrorMessage( allErrors, 412 ); 271 | 272 | // Render Error Out 273 | event.renderData( 274 | type = prc.response.getFormat(), 275 | data = prc.response.getDataPacket(), 276 | contentType = prc.response.getContentType(), 277 | statusCode = prc.response.getStatusCode(), 278 | statusText = prc.response.getStatusText(), 279 | location = prc.response.getLocation(), 280 | isBinary = prc.response.getBinary() 281 | ); 282 | } 283 | 284 | /** 285 | * on invalid http verbs 286 | */ 287 | function onInvalidHTTPMethod( 288 | event, 289 | rc, 290 | prc, 291 | faultAction, 292 | eventArguments 293 | ) { 294 | // Log Locally 295 | log.warn( 296 | "InvalidHTTPMethod Execution of (#arguments.faultAction#): #event.getHTTPMethod()#", 297 | getHTTPRequestData() 298 | ); 299 | 300 | // Setup Response 301 | prc.response = getModel( "Response@api" ) 302 | .setError( true ) 303 | .addMessage( "InvalidHTTPMethod Execution of (#arguments.faultAction#): #event.getHTTPMethod()#" ) 304 | .setStatusCode( STATUS.NOT_ALLOWED ) 305 | .setStatusText( "Invalid HTTP Method" ); 306 | 307 | // Render Error Out 308 | event.renderData( 309 | type = prc.response.getFormat(), 310 | data = prc.response.getDataPacket(), 311 | contentType = prc.response.getContentType(), 312 | statusCode = prc.response.getStatusCode(), 313 | statusText = prc.response.getStatusText(), 314 | location = prc.response.getLocation(), 315 | isBinary = prc.response.getBinary() 316 | ); 317 | } 318 | 319 | /** 320 | * When missing actions are executed 321 | */ 322 | function onMissingAction( 323 | event, 324 | rc, 325 | prc, 326 | missingAction, 327 | eventArguments 328 | ) { 329 | // Setup Response 330 | prc.response = getModel( "Response@api" ) 331 | .setError( true ) 332 | .addMessage( "Action '#arguments.missingAction#' could not be found" ) 333 | .setStatusCode( STATUS.NOT_ALLOWED ) 334 | .setStatusText( "Invalid Action" ); 335 | 336 | // Render Error Out 337 | event.renderData( 338 | type = prc.response.getFormat(), 339 | data = prc.response.getDataPacket(), 340 | contentType = prc.response.getContentType(), 341 | statusCode = prc.response.getStatusCode(), 342 | statusText = prc.response.getStatusText(), 343 | location = prc.response.getLocation(), 344 | isBinary = prc.response.getBinary() 345 | ); 346 | } 347 | 348 | /** 349 | * Executed on authentication failures 350 | */ 351 | function onAuthenticationFailure( 352 | event = getRequestContext(), 353 | rc = getRequestCollection(), 354 | prc = getPrivateCollection(), 355 | abort = false 356 | ) { 357 | if ( !structKeyExists( prc, "Response" ) ) { 358 | prc.response = getModel( "Response@api" ); 359 | } 360 | 361 | // case when the a jwt token was valid, but expired 362 | if ( 363 | !isNull( prc.cbSecurity_validatorResults ) && 364 | prc.cbSecurity_validatorResults.messages CONTAINS "expired" 365 | ) { 366 | prc.response 367 | .setError( true ) 368 | .setStatusCode( STATUS.NOT_AUTHENTICATED ) 369 | .setStatusText( "Expired Authentication Credentials" ) 370 | .addMessage( "Expired Authentication Credentials" ); 371 | return; 372 | } 373 | 374 | prc.response 375 | .setError( true ) 376 | .setStatusCode( STATUS.NOT_AUTHENTICATED ) 377 | .setStatusText( "Invalid or Missing Credentials" ) 378 | .addMessage( "Invalid or Missing Authentication Credentials" ); 379 | } 380 | 381 | /** 382 | * Executed on authorization failures 383 | */ 384 | function onAuthorizationFailure( 385 | event = getRequestContext(), 386 | rc = getRequestCollection(), 387 | prc = getPrivateCollection(), 388 | abort = false 389 | ) { 390 | if ( !structKeyExists( prc, "Response" ) ) { 391 | prc.response = getModel( "Response@api" ); 392 | } 393 | 394 | prc.response 395 | .setError( true ) 396 | .setStatusCode( STATUS.NOT_AUTHORIZED ) 397 | .setStatusText( "Unauthorized Resource" ) 398 | .addMessage( "Your permissions do not allow this operation" ); 399 | 400 | // Check for validator results 401 | if ( !isNull( prc.cbSecurity_validatorResults ) ) { 402 | prc.response.addMessage( prc.cbSecurity_validatorResults.messages ); 403 | } 404 | 405 | /** 406 | * When you need a really hard stop to prevent further execution ( use as last resort ) 407 | */ 408 | if ( arguments.abort ) { 409 | event.setHTTPHeader( name = "Content-Type", value = "application/json" ); 410 | 411 | event.setHTTPHeader( statusCode = "#STATUS.NOT_AUTHORIZED#", statusText = "Not Authorized" ); 412 | 413 | writeOutput( serializeJSON( prc.response.getDataPacket() ) ); 414 | 415 | flush; 416 | abort; 417 | } 418 | } 419 | 420 | /** 421 | * Resource Not Found 422 | */ 423 | function onInvalidRoute( event, rc, prc ) { 424 | if ( !structKeyExists( prc, "Response" ) ) { 425 | prc.response = getModel( "Response@api" ); 426 | } 427 | 428 | prc.response 429 | .setError( true ) 430 | .setStatusCode( STATUS.NOT_FOUND ) 431 | .setStatusText( "Not Found" ) 432 | .addMessage( "The resource requested (#event.getCurrentRoutedURL()#) could not be found" ); 433 | } 434 | 435 | /**************************** RESTFUL UTILITIES ************************/ 436 | 437 | /** 438 | * Utility method for when an expectation of the request failes ( e.g. an expected paramter is not provided ) 439 | */ 440 | private function onExpectationFailed( 441 | event = getRequestContext(), 442 | rc = getRequestCollection(), 443 | prc = getRequestCollection( private = true ) 444 | ) { 445 | if ( !structKeyExists( prc, "Response" ) ) { 446 | prc.response = getModel( "Response@api" ); 447 | } 448 | 449 | prc.response 450 | .setError( true ) 451 | .setStatusCode( STATUS.EXPECTATION_FAILED ) 452 | .setStatusText( "Expectation Failed" ) 453 | .addMessage( "An expectation for the request failed. Could not proceed" ); 454 | } 455 | 456 | } 457 | -------------------------------------------------------------------------------- /modules_app/api/models/Response.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * ******************************************************************************** 3 | * Copyright 2005-2007 ColdBox Framework by Luis Majano and Ortus Solutions, Corp 4 | * www.ortussolutions.com 5 | * ******************************************************************************** 6 | * HTTP Response model, spice up as needed and stored in the request scope 7 | */ 8 | component accessors="true" { 9 | 10 | property name="format" type="string" default="json"; 11 | property name="data" type="any" default=""; 12 | property name="error" type="boolean" default="false"; 13 | property name="binary" type="boolean" default="false"; 14 | property name="messages" type="array"; 15 | property name="location" type="string" default=""; 16 | property name="jsonCallback" type="string" default=""; 17 | property 18 | name="jsonQueryFormat" 19 | type="string" 20 | default="true" 21 | hint="JSON Only: This parameter can be a Boolean value that specifies how to serialize ColdFusion queries or a string with possible values row, column, or struct"; 22 | property name="contentType" type="string" default=""; 23 | property name="statusCode" type="numeric" default="200"; 24 | property name="statusText" type="string" default="OK"; 25 | property name="responsetime" type="numeric" default="0"; 26 | property name="cachedResponse" type="boolean" default="false"; 27 | property name="headers" type="array"; 28 | 29 | /** 30 | * Constructor 31 | */ 32 | Response function init() { 33 | // Init properties 34 | variables.format = "json"; 35 | variables.data = {}; 36 | variables.pagination = {}; 37 | variables.error = false; 38 | variables.binary = false; 39 | variables.messages = []; 40 | variables.location = ""; 41 | variables.jsonCallBack = ""; 42 | variables.jsonQueryFormat = "query"; 43 | variables.contentType = ""; 44 | variables.statusCode = 200; 45 | variables.statusText = "OK"; 46 | variables.responsetime = 0; 47 | variables.cachedResponse = false; 48 | variables.headers = []; 49 | 50 | return this; 51 | } 52 | 53 | STATUS_TEXTS = { 54 | "100": "Continue", 55 | "101": "Switching Protocols", 56 | "102": "Processing", 57 | "200": "OK", 58 | "201": "Created", 59 | "202": "Accepted", 60 | "203": "Non-authoritative Information", 61 | "204": "No Content", 62 | "205": "Reset Content", 63 | "206": "Partial Content", 64 | "207": "Multi-Status", 65 | "208": "Already Reported", 66 | "226": "IM Used", 67 | "300": "Multiple Choices", 68 | "301": "Moved Permanently", 69 | "302": "Found", 70 | "303": "See Other", 71 | "304": "Not Modified", 72 | "305": "Use Proxy", 73 | "307": "Temporary Redirect", 74 | "308": "Permanent Redirect", 75 | "400": "Bad Request", 76 | "401": "Unauthorized", 77 | "402": "Payment Required", 78 | "403": "Forbidden", 79 | "404": "Not Found", 80 | "405": "Method Not Allowed", 81 | "406": "Not Acceptable", 82 | "407": "Proxy Authentication Required", 83 | "408": "Request Timeout", 84 | "409": "Conflict", 85 | "410": "Gone", 86 | "411": "Length Required", 87 | "412": "Precondition Failed", 88 | "413": "Payload Too Large", 89 | "414": "Request-URI Too Long", 90 | "415": "Unsupported Media Type", 91 | "416": "Requested Range Not Satisfiable", 92 | "417": "Expectation Failed", 93 | "418": "I'm a teapot", 94 | "421": "Misdirected Request", 95 | "422": "Unprocessable Entity", 96 | "423": "Locked", 97 | "424": "Failed Dependency", 98 | "426": "Upgrade Required", 99 | "428": "Precondition Required", 100 | "429": "Too Many Requests", 101 | "431": "Request Header Fields Too Large", 102 | "444": "Connection Closed Without Response", 103 | "451": "Unavailable For Legal Reasons", 104 | "499": "Client Closed Request", 105 | "500": "Internal Server Error", 106 | "501": "Not Implemented", 107 | "502": "Bad Gateway", 108 | "503": "Service Unavailable", 109 | "504": "Gateway Timeout", 110 | "505": "HTTP Version Not Supported", 111 | "506": "Variant Also Negotiates", 112 | "507": "Insufficient Storage", 113 | "508": "Loop Detected", 114 | "510": "Not Extended", 115 | "511": "Network Authentication Required", 116 | "599": "Network Connect Timeout Error" 117 | }; 118 | 119 | /** 120 | * Sets the status code with a statusText for the API response 121 | * @statusCode The status code to be set 122 | * @statusText The status text to be set 123 | * 124 | * @return Returns the Response object for chaining 125 | */ 126 | function setStatusCode( required statusCode, statusText ) { 127 | if ( isNull( arguments.statusText ) ) { 128 | if ( structKeyExists( variables.STATUS_TEXTS, arguments.statusCode ) ) { 129 | arguments.statusText = variables.STATUS_TEXTS[ arguments.statusCode ]; 130 | } else { 131 | arguments.statusText = ""; 132 | } 133 | } 134 | variables.statusCode = arguments.statusCode; 135 | variables.statusText = arguments.statusText; 136 | return this; 137 | } 138 | 139 | /** 140 | * Sets the data and pagination from a struct with `results` and `pagination`. 141 | * 142 | * @data The struct containing both results and pagination. 143 | * @resultsKey The name of the key with the results. 144 | * @paginationKey The name of the key with the pagination. 145 | */ 146 | function setDataWithPagination( data, resultsKey = "results", paginationKey = "pagination" ) { 147 | variables.data = arguments.data[ arguments.resultsKey ]; 148 | variables.pagination = arguments.data[ arguments.paginationKey ]; 149 | return this; 150 | } 151 | 152 | /** 153 | * Sets the error message with a code for the API response 154 | * @errorMessage The errorMessage to be set 155 | * @statusCode The status code to be set 156 | * @statusText The status text to be set 157 | * 158 | * @return Returns the Response object for chaining 159 | 160 | */ 161 | function setErrorMessage( required errorMessage, statusCode, statusText ) { 162 | setError( true ); 163 | addMessage( arguments.errorMessage ); 164 | if ( !isNull( arguments.statusCode ) ) { 165 | setStatusCode( arguments.statusCode, !isNull( arguments.statusText ) ? arguments.statusText : "" ); 166 | } else { 167 | if ( !isNull( arguments.statusText ) ) { 168 | setStatusText( arguments.statusText ); 169 | } 170 | } 171 | return this; 172 | } 173 | 174 | 175 | /** 176 | * Add some messages 177 | * @message Array or string of message to incorporate 178 | */ 179 | function addMessage( required any message ) { 180 | if ( isSimpleValue( arguments.message ) ) { 181 | arguments.message = [ arguments.message ]; 182 | } 183 | variables.messages.addAll( arguments.message ); 184 | return this; 185 | } 186 | 187 | /** 188 | * Add a header 189 | * @name The header name ( e.g. "Content-Type" ) 190 | * @value The header value ( e.g. "application/json" ) 191 | */ 192 | function addHeader( required string name, required string value ) { 193 | arrayAppend( variables.headers, { name: arguments.name, value: arguments.value } ); 194 | return this; 195 | } 196 | 197 | /** 198 | * Returns a standard response formatted data packet 199 | * @reset Reset the 'data' element of the original data packet 200 | */ 201 | function getDataPacket( boolean reset = false ) { 202 | var packet = { 203 | "error": variables.error ? true : false, 204 | "messages": variables.messages, 205 | "data": variables.data, 206 | "pagination": variables.pagination 207 | }; 208 | 209 | // Are we reseting the data packet 210 | if ( arguments.reset ) { 211 | packet.data = {}; 212 | } 213 | 214 | return packet; 215 | } 216 | 217 | } 218 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v1/ModuleConfig.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | Module Directives as public properties 3 | this.title = "Title of the module"; 4 | this.author = "Author of the module"; 5 | this.webURL = "Web URL for docs purposes"; 6 | this.description = "Module description"; 7 | this.version = "Module Version"; 8 | this.viewParentLookup = (true) [boolean] (Optional) // If true, checks for views in the parent first, then it the module.If false, then modules first, then parent. 9 | this.layoutParentLookup = (true) [boolean] (Optional) // If true, checks for layouts in the parent first, then it the module.If false, then modules first, then parent. 10 | this.entryPoint = "" (Optional) // If set, this is the default event (ex:forgebox:manager.index) or default route (/forgebox) the framework 11 | will use to create an entry link to the module. Similar to a default event. 12 | this.cfmapping = "The CF mapping to create"; 13 | this.modelNamespace = "The namespace to use for registered models, if blank it uses the name of the module." 14 | this.dependencies = "The array of dependencies for this module" 15 | 16 | structures to create for configuration 17 | - parentSettings : struct (will append and override parent) 18 | - settings : struct 19 | - interceptorSettings : struct of the following keys ATM 20 | - customInterceptionPoints : string list of custom interception points 21 | - interceptors : array 22 | - layoutSettings : struct (will allow to define a defaultLayout for the module) 23 | - routes : array Allowed keys are same as the addRoute() method of the SES interceptor. 24 | - wirebox : The wirebox DSL to load and use 25 | 26 | Available objects in variable scope 27 | - controller 28 | - appMapping (application mapping) 29 | - moduleMapping (include,cf path) 30 | - modulePath (absolute path) 31 | - log (A pre-configured logBox logger object for this object) 32 | - binder (The wirebox configuration binder) 33 | - wirebox (The wirebox injector) 34 | 35 | Required Methods 36 | - configure() : The method ColdBox calls to configure the module. 37 | 38 | Optional Methods 39 | - onLoad() : If found, it is fired once the module is fully loaded 40 | - onUnload() : If found, it is fired once the module is unloaded 41 | 42 | */ 43 | component { 44 | 45 | // Module Properties 46 | this.title = "v1"; 47 | this.author = ""; 48 | this.webURL = ""; 49 | this.description = ""; 50 | this.version = "1.0.0"; 51 | // If true, looks for views in the parent first, if not found, then in the module. Else vice-versa 52 | this.viewParentLookup = true; 53 | // If true, looks for layouts in the parent first, if not found, then in module. Else vice-versa 54 | this.layoutParentLookup = true; 55 | // Module Entry Point 56 | this.entryPoint = "v1"; 57 | // Inherit entry point from parent, so this will be /api/v1 58 | this.inheritEntryPoint = true; 59 | // Model Namespace 60 | this.modelNamespace = "v1"; 61 | // CF Mapping 62 | this.cfmapping = "v1"; 63 | // Auto-map models 64 | this.autoMapModels = true; 65 | // Module Dependencies 66 | this.dependencies = []; 67 | 68 | function configure() { 69 | // parent settings 70 | parentSettings = {}; 71 | 72 | // module settings - stored in modules.name.settings 73 | settings = {}; 74 | 75 | // Layout Settings 76 | layoutSettings = { defaultLayout: "" }; 77 | 78 | // SES Routes: config/Router.cfc 79 | 80 | // Custom Declared Points 81 | interceptorSettings = { customInterceptionPoints: "" }; 82 | 83 | // Custom Declared Interceptors 84 | interceptors = []; 85 | 86 | // Binder Mappings 87 | // binder.map("Alias").to("#moduleMapping#.model.MyService"); 88 | } 89 | 90 | /** 91 | * Fired when the module is registered and activated. 92 | */ 93 | function onLoad() { 94 | } 95 | 96 | /** 97 | * Fired when the module is unregistered and unloaded 98 | */ 99 | function onUnload() { 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v1/config/Router.cfc: -------------------------------------------------------------------------------- 1 | component { 2 | 3 | function configure() { 4 | route( "/", "echo.index" ); 5 | route( "/:handler/:action" ).end(); 6 | } 7 | 8 | } 9 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v1/handlers/Echo.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * My RESTFul Event Handler which inherits from the module `api` 3 | */ 4 | component extends="api.handlers.BaseHandler" { 5 | 6 | // OPTIONAL HANDLER PROPERTIES 7 | this.prehandler_only = ""; 8 | this.prehandler_except = ""; 9 | this.posthandler_only = ""; 10 | this.posthandler_except = ""; 11 | this.aroundHandler_only = ""; 12 | this.aroundHandler_except = ""; 13 | 14 | // REST Allowed HTTP Methods Ex: this.allowedMethods = {delete='POST,DELETE',index='GET'} 15 | this.allowedMethods = {}; 16 | 17 | /** 18 | * Index 19 | */ 20 | any function index( event, rc, prc ) { 21 | prc.response.setData( "Welcome to my ColdBox RESTFul Service V1" ); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v1/handlers/Rants.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * My RESTFul Rants Event Handler which inherits from the module `api` 3 | */ 4 | component extends="api.handlers.BaseHandler" { 5 | 6 | // DI 7 | property name="rantService" inject="RantService@v1"; 8 | property name="userService" inject="UserService@v1"; 9 | 10 | 11 | /** 12 | * Returns a list of Rants 13 | */ 14 | any function list( event, rc, prc ) { 15 | var rants = rantService.list(); 16 | prc.response.setData( rants ); 17 | } 18 | 19 | 20 | /** 21 | * Returns a single Rant 22 | * 23 | */ 24 | function view( event, rc, prc ) { 25 | if ( !structKeyExists( rc, "rantID" ) ) { 26 | prc.response.setError( true ); 27 | prc.response.setStatusCode( 412 ); 28 | prc.response.addMessage( "rantID is required" ); 29 | return; 30 | } 31 | if ( !isNumeric( rc.rantID ) ) { 32 | prc.response.setError( true ); 33 | prc.response.setStatusCode( 412 ); 34 | prc.response.addMessage( "rantID must be numeric" ); 35 | return; 36 | } 37 | var rant = rantService.getRant( rc.rantID ) 38 | 39 | if ( rant.len() ) { 40 | prc.response.setData( deserializeJSON( serializeJSON( rant[ 1 ], "struct" ) ) ) 41 | } else { 42 | prc.response.setError( true ); 43 | prc.response.setStatusCode( 404 ); 44 | prc.response.addMessage( "Error loading Rant - Rant not found" ); 45 | } 46 | } 47 | 48 | /** 49 | * Deletes a single Rant 50 | * 51 | */ 52 | function delete( event, rc, prc ) { 53 | if ( !structKeyExists( rc, "rantID" ) ) { 54 | prc.response.setError( true ); 55 | prc.response.setStatusCode( 412 ); 56 | prc.response.addMessage( "rantID is required" ); 57 | } else if ( !isNumeric( rc.rantID ) ) { 58 | prc.response.setError( true ); 59 | prc.response.setStatusCode( 412 ); 60 | prc.response.addMessage( "rantID must be numeric" ); 61 | } else { 62 | var result = rantService.delete( rc.rantID ); 63 | if ( result.recordcount > 0 ) { 64 | prc.response.addMessage( "Rant deleted" ); 65 | } else { 66 | prc.response.setError( true ); 67 | prc.response.setStatusCode( 404 ); 68 | prc.response.addMessage( "Rant not found" ); 69 | } 70 | } 71 | } 72 | 73 | /** 74 | * Creates a new Rant 75 | * 76 | */ 77 | function create( event, rc, prc ) { 78 | if ( !structKeyExists( rc, "body" ) ) { 79 | prc.response.setError( true ); 80 | prc.response.setStatusCode( 412 ); 81 | prc.response.addMessage( "Rant body is required" ); 82 | return 83 | } 84 | if ( !rc.body.trim().len() ) { 85 | prc.response.setError( true ); 86 | prc.response.setStatusCode( 412 ); 87 | prc.response.addMessage( "Rant body cannot be empty" ); 88 | return 89 | } 90 | if ( !structKeyExists( rc, "userID" ) ) { 91 | prc.response.setError( true ); 92 | prc.response.setStatusCode( 412 ); 93 | prc.response.addMessage( "userID is required" ); 94 | return; 95 | } 96 | if ( !isNumeric( rc.userID ) ) { 97 | prc.response.setError( true ); 98 | prc.response.setStatusCode( 412 ); 99 | prc.response.addMessage( "userID must be numeric" ); 100 | return; 101 | } 102 | var user = userService.get( rc.userID ) 103 | if ( !user.len() ) { 104 | prc.response.setError( true ); 105 | prc.response.setStatusCode( 404 ); 106 | prc.response.addMessage( "User not found" ); 107 | return; 108 | } 109 | var result = rantService.create( body = rc.body, userID = rc.userID ); 110 | if ( result.recordcount ) { 111 | prc.response.setData( { "rantID": result.generatedKey } ); 112 | prc.response.addMessage( "Rant created" ); 113 | return; 114 | } else { 115 | prc.response.setError( true ); 116 | prc.response.setStatusCode( 500 ); 117 | prc.response.addMessage( "Error creating Rant" ); 118 | return; 119 | } 120 | } 121 | 122 | /** 123 | * Updates an Existing Rant 124 | * 125 | */ 126 | function save( event, rc, prc ) { 127 | if ( !structKeyExists( rc, "body" ) ) { 128 | prc.response.setError( true ); 129 | prc.response.setStatusCode( 412 ); 130 | prc.response.addMessage( "Rant body is required" ); 131 | return 132 | } 133 | if ( !rc.body.trim().len() ) { 134 | prc.response.setError( true ); 135 | prc.response.setStatusCode( 412 ); 136 | prc.response.addMessage( "Rant body cannot be empty" ); 137 | return 138 | } 139 | if ( !structKeyExists( rc, "rantID" ) ) { 140 | prc.response.setError( true ); 141 | prc.response.setStatusCode( 412 ); 142 | prc.response.addMessage( "rantID is required" ); 143 | return 144 | } 145 | if ( !isNumeric( rc.rantID ) ) { 146 | prc.response.setError( true ); 147 | prc.response.setStatusCode( 412 ); 148 | prc.response.addMessage( "rantID must be numeric" ); 149 | return 150 | } 151 | var rant = rantService.getRant( rc.rantID ) 152 | if ( !rant.len() ) { 153 | prc.response.setError( true ); 154 | prc.response.setStatusCode( 404 ); 155 | prc.response.addMessage( "Rant not found" ); 156 | return; 157 | } 158 | if ( !structKeyExists( rc, "userID" ) ) { 159 | prc.response.setError( true ); 160 | prc.response.setStatusCode( 412 ); 161 | prc.response.addMessage( "userID is required" ); 162 | return; 163 | } 164 | if ( !isNumeric( rc.userID ) ) { 165 | prc.response.setError( true ); 166 | prc.response.setStatusCode( 412 ); 167 | prc.response.addMessage( "userID must be numeric" ); 168 | return; 169 | } 170 | var user = userService.get( rc.userID ) 171 | if ( !user.len() ) { 172 | prc.response.setError( true ); 173 | prc.response.setStatusCode( 404 ); 174 | prc.response.addMessage( "User not found" ); 175 | return; 176 | } 177 | var result = rantService.update( body = rc.body, userID = rc.userID, rantID = rc.rantID ); 178 | if ( result.recordcount ) { 179 | prc.response.addMessage( "Rant Updated" ); 180 | } else { 181 | prc.response.setError( true ); 182 | prc.response.setStatusCode( 500 ); 183 | prc.response.addMessage( "Error updating Rant" ); 184 | return; 185 | } 186 | } 187 | 188 | } 189 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v1/models/RantService.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * I am the Rant Service 3 | */ 4 | component singleton accessors="true" { 5 | 6 | /** 7 | * Constructor 8 | */ 9 | RantService function init() { 10 | return this; 11 | } 12 | 13 | function list() { 14 | return queryExecute( "select * from rants ORDER BY createdDate DESC", {}, { returntype: "array" } ); 15 | } 16 | 17 | function getRant( required numeric rantID ) { 18 | return queryExecute( 19 | "select * from rants 20 | where id = :rantID", 21 | { rantID: { value: "#rantID#", type: "cf_sql_numeric" } }, 22 | { returntype: "array" } 23 | ); 24 | } 25 | 26 | function delete( required numeric rantID ) { 27 | queryExecute( 28 | "delete from rants 29 | where id = :rantID", 30 | { rantID: { value: "#rantID#", type: "cf_sql_numeric" } }, 31 | { result: "local.result" } 32 | ); 33 | return local.result; 34 | } 35 | 36 | function create( required body, required numeric userID ) { 37 | var now = now(); 38 | queryExecute( 39 | "insert into rants 40 | set 41 | body = :body, 42 | userID = :userID, 43 | createdDate = :createdDate, 44 | modifiedDate = :modifiedDate 45 | ", 46 | { 47 | body: { value: "#body#", type: "cf_sql_longvarchar" }, 48 | userID: { value: "#userID#", type: "cf_sql_numeric" }, 49 | createdDate: { value: "#now#", type: "cf_sql_timestamp" }, 50 | modifiedDate: { value: "#now#", type: "cf_sql_timestamp" } 51 | }, 52 | { result: "local.result" } 53 | ); 54 | return local.result; 55 | } 56 | 57 | function update( required body, ) { 58 | var now = now(); 59 | queryExecute( 60 | "update rants 61 | set 62 | body = :body, 63 | modifiedDate = :modifiedDate 64 | where id = :rantID 65 | ", 66 | { 67 | rantID: { value: "#rantID#", type: "cf_sql_integer" }, 68 | body: { value: "#body#", type: "cf_sql_longvarchar" }, 69 | modifiedDate: { value: "#now#", type: "cf_sql_timestamp" } 70 | }, 71 | { result: "local.result" } 72 | ); 73 | return local.result; 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v1/models/UserService.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * I am the User Service 3 | */ 4 | component singleton accessors="true" { 5 | 6 | /** 7 | * Constructor 8 | */ 9 | UserService function init() { 10 | return this; 11 | } 12 | 13 | function get( required numeric userID ) { 14 | return queryExecute( 15 | "select * from users 16 | where id = :userID", 17 | { userID: { value: "#userID#", type: "cf_sql_numeric" } }, 18 | { returntype: "array" } 19 | ); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v2/ModuleConfig.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | Module Directives as public properties 3 | this.title = "Title of the module"; 4 | this.author = "Author of the module"; 5 | this.webURL = "Web URL for docs purposes"; 6 | this.description = "Module description"; 7 | this.version = "Module Version"; 8 | this.viewParentLookup = (true) [boolean] (Optional) // If true, checks for views in the parent first, then it the module.If false, then modules first, then parent. 9 | this.layoutParentLookup = (true) [boolean] (Optional) // If true, checks for layouts in the parent first, then it the module.If false, then modules first, then parent. 10 | this.entryPoint = "" (Optional) // If set, this is the default event (ex:forgebox:manager.index) or default route (/forgebox) the framework 11 | will use to create an entry link to the module. Similar to a default event. 12 | this.cfmapping = "The CF mapping to create"; 13 | this.modelNamespace = "The namespace to use for registered models, if blank it uses the name of the module." 14 | this.dependencies = "The array of dependencies for this module" 15 | 16 | structures to create for configuration 17 | - parentSettings : struct (will append and override parent) 18 | - settings : struct 19 | - interceptorSettings : struct of the following keys ATM 20 | - customInterceptionPoints : string list of custom interception points 21 | - interceptors : array 22 | - layoutSettings : struct (will allow to define a defaultLayout for the module) 23 | - routes : array Allowed keys are same as the addRoute() method of the SES interceptor. 24 | - wirebox : The wirebox DSL to load and use 25 | 26 | Available objects in variable scope 27 | - controller 28 | - appMapping (application mapping) 29 | - moduleMapping (include,cf path) 30 | - modulePath (absolute path) 31 | - log (A pre-configured logBox logger object for this object) 32 | - binder (The wirebox configuration binder) 33 | - wirebox (The wirebox injector) 34 | 35 | Required Methods 36 | - configure() : The method ColdBox calls to configure the module. 37 | 38 | Optional Methods 39 | - onLoad() : If found, it is fired once the module is fully loaded 40 | - onUnload() : If found, it is fired once the module is unloaded 41 | 42 | */ 43 | component { 44 | 45 | // Module Properties 46 | this.title = "v2"; 47 | this.author = ""; 48 | this.webURL = ""; 49 | this.description = ""; 50 | this.version = "1.0.0"; 51 | // If true, looks for views in the parent first, if not found, then in the module. Else vice-versa 52 | this.viewParentLookup = true; 53 | // If true, looks for layouts in the parent first, if not found, then in module. Else vice-versa 54 | this.layoutParentLookup = true; 55 | // Module Entry Point 56 | this.entryPoint = "v2"; 57 | // Inherit entry point from parent, so this will be /api/v1 58 | this.inheritEntryPoint = true; 59 | // Model Namespace 60 | this.modelNamespace = "v2"; 61 | // CF Mapping 62 | this.cfmapping = "v2"; 63 | // Auto-map models 64 | this.autoMapModels = true; 65 | // Module Dependencies 66 | this.dependencies = []; 67 | 68 | function configure() { 69 | // parent settings 70 | parentSettings = {}; 71 | 72 | // module settings - stored in modules.name.settings 73 | settings = {}; 74 | 75 | // Layout Settings 76 | layoutSettings = { defaultLayout: "" }; 77 | 78 | // SES Routes: config/Router.cfc 79 | 80 | // Custom Declared Points 81 | interceptorSettings = { customInterceptionPoints: "" }; 82 | 83 | // Custom Declared Interceptors 84 | interceptors = []; 85 | 86 | // Binder Mappings 87 | // binder.map("Alias").to("#moduleMapping#.model.MyService"); 88 | } 89 | 90 | /** 91 | * Fired when the module is registered and activated. 92 | */ 93 | function onLoad() { 94 | } 95 | 96 | /** 97 | * Fired when the module is unregistered and unloaded 98 | */ 99 | function onUnload() { 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v2/config/Router.cfc: -------------------------------------------------------------------------------- 1 | component { 2 | 3 | function configure() { 4 | post( "/rants/create", "rants.create" ) 5 | route( "/rants/:rantID/delete", "rants.delete" ) 6 | route( "/rants/:rantID/save", "rants.save" ) 7 | get( "/rants/:rantID", "rants.view" ) 8 | route( "/rants", "rants.list" ) 9 | route( "/", "echo.index" ); 10 | route( "/:handler/:action" ).end(); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v2/handlers/Echo.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * My RESTFul Event Handler which inherits from the module `api` 3 | */ 4 | component extends="api.handlers.BaseHandler" { 5 | 6 | // OPTIONAL HANDLER PROPERTIES 7 | this.prehandler_only = ""; 8 | this.prehandler_except = ""; 9 | this.posthandler_only = ""; 10 | this.posthandler_except = ""; 11 | this.aroundHandler_only = ""; 12 | this.aroundHandler_except = ""; 13 | 14 | // REST Allowed HTTP Methods Ex: this.allowedMethods = {delete='POST,DELETE',index='GET'} 15 | this.allowedMethods = {}; 16 | 17 | /** 18 | * Index 19 | */ 20 | any function index( event, rc, prc ) { 21 | prc.response.setData( "Welcome to my ColdBox RESTFul Service V2" ); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v2/handlers/Rants.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * My RESTFul Rants Event Handler which inherits from the module `api` 3 | */ 4 | component extends="api.handlers.BaseHandler" { 5 | 6 | // DI 7 | property name="rantService" inject="RantService@v2"; 8 | property name="userService" inject="UserService@v2"; 9 | 10 | 11 | /** 12 | * Returns a list of Rants 13 | */ 14 | any function list( event, rc, prc ) { 15 | prc.response.setData( rantService.list() ); 16 | } 17 | 18 | 19 | /** 20 | * Returns a single Rant 21 | * 22 | */ 23 | function view( event, rc, prc ) { 24 | var validationResults = validate( target = rc, constraints = { rantID: { required: true, type: "numeric" } } ); 25 | if ( validationResults.hasErrors() ) { 26 | prc.response.setErrorMessage( validationResults.getAllErrors(), 412 ); 27 | return; 28 | } 29 | var rant = rantService.get( rc.rantID ) 30 | if ( rant.len() ) { 31 | prc.response.setData( deserializeJSON( serializeJSON( rant[ 1 ], "struct" ) ) ) 32 | } else { 33 | prc.response.setErrorMessage( "Error loading Rant - Rant not found", 404 ); 34 | } 35 | } 36 | 37 | /** 38 | * Deletes a single Rant 39 | * 40 | */ 41 | function delete( event, rc, prc ) { 42 | var validationResults = validate( target = rc, constraints = { rantID: { required: true, type: "numeric" } } ); 43 | if ( validationResults.hasErrors() ) { 44 | prc.response.setErrorMessage( validationResults.getAllErrors(), 412 ); 45 | return; 46 | } 47 | 48 | var result = rantService.delete( rc.rantID ); 49 | if ( result.recordcount > 0 ) { 50 | prc.response.addMessage( "Rant deleted" ); 51 | } else { 52 | prc.response.setErrorMessage( "Rant not found", 404 ); 53 | } 54 | } 55 | 56 | /** 57 | * Creates a new Rant 58 | * 59 | */ 60 | function create( event, rc, prc ) { 61 | var validationResults = validate( 62 | target = rc, 63 | constraints = { userID: { required: true, type: "numeric" }, body: { required: true } } 64 | ); 65 | if ( validationResults.hasErrors() ) { 66 | prc.response.setErrorMessage( validationResults.getAllErrors(), 412 ); 67 | return; 68 | } 69 | if ( !userService.exists( rc.userID ) ) { 70 | prc.response.setErrorMessage( "User not found", 404 ); 71 | return; 72 | } 73 | var result = rantService.create( body = rc.body, userID = rc.userID ); 74 | if ( result.recordcount ) { 75 | prc.response.setData( { "rantID": result.generatedKey } ); 76 | prc.response.addMessage( "Rant created" ); 77 | return; 78 | } else { 79 | prc.response.setErrorMessage( "Error creating Rant", 500 ); 80 | return; 81 | } 82 | } 83 | 84 | /** 85 | * Updates an Existing Rant 86 | * 87 | */ 88 | function save( event, rc, prc ) { 89 | var validationResults = validate( 90 | target = rc, 91 | constraints = { 92 | rantID: { required: true, type: "numeric" }, 93 | body: { required: true }, 94 | userID: { required: true, type: "numeric" } 95 | } 96 | ); 97 | if ( validationResults.hasErrors() ) { 98 | prc.response.setErrorMessage( validationResults.getAllErrors(), 412 ); 99 | return; 100 | } 101 | 102 | if ( !rantService.exists( rc.rantID ) ) { 103 | prc.response.setErrorMessage( "Rant not found", 404 ); 104 | return; 105 | } 106 | if ( !userService.exists( rc.userID ) ) { 107 | prc.response.setErrorMessage( "User not found", 404 ); 108 | return; 109 | } 110 | var result = rantService.update( body = rc.body, userID = rc.userID, rantID = rc.rantID ); 111 | if ( result.recordcount ) { 112 | prc.response.addMessage( "Rant Updated" ); 113 | } else { 114 | prc.response.setErrorMessage( "Error updating Rant", 500 ); 115 | return; 116 | } 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v2/models/RantService.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * I am the Rant Service V2 3 | */ 4 | component singleton accessors="true" { 5 | 6 | /** 7 | * Constructor 8 | */ 9 | RantService function init() { 10 | return this; 11 | } 12 | 13 | function list() { 14 | return queryExecute( "select * from rants ORDER BY createdDate DESC", {}, { returntype: "array" } ); 15 | } 16 | 17 | function get( required numeric rantID ) { 18 | return queryExecute( 19 | "select * from rants 20 | where id = :rantID", 21 | { rantID: { value: "#rantID#", type: "cf_sql_numeric" } }, 22 | { returntype: "array" } 23 | ); 24 | } 25 | 26 | function delete( required numeric rantID ) { 27 | queryExecute( 28 | "delete from rants 29 | where id = :rantID", 30 | { rantID: { value: "#rantID#", type: "cf_sql_numeric" } }, 31 | { result: "local.result" } 32 | ); 33 | return local.result; 34 | } 35 | 36 | function create( required body, required numeric userID ) { 37 | var now = now(); 38 | queryExecute( 39 | "insert into rants 40 | set 41 | body = :body, 42 | userID = :userID, 43 | createdDate = :createdDate, 44 | modifiedDate = :modifiedDate 45 | ", 46 | { 47 | body: { value: "#body#", type: "cf_sql_longvarchar" }, 48 | userID: { value: "#userID#", type: "cf_sql_numeric" }, 49 | createdDate: { value: "#now#", type: "cf_sql_timestamp" }, 50 | modifiedDate: { value: "#now#", type: "cf_sql_timestamp" } 51 | }, 52 | { result: "local.result" } 53 | ); 54 | return local.result; 55 | } 56 | 57 | function update( required body, ) { 58 | var now = now(); 59 | queryExecute( 60 | "update rants 61 | set 62 | body = :body, 63 | modifiedDate = :modifiedDate 64 | where id = :rantID 65 | ", 66 | { 67 | rantID: { value: "#rantID#", type: "cf_sql_integer" }, 68 | body: { value: "#body#", type: "cf_sql_longvarchar" }, 69 | modifiedDate: { value: "#now#", type: "cf_sql_timestamp" } 70 | }, 71 | { result: "local.result" } 72 | ); 73 | return local.result; 74 | } 75 | 76 | boolean function exists( required numeric rantID ) { 77 | return booleanFormat( 78 | queryExecute( 79 | "select id from rants 80 | where id = :rantID", 81 | { rantID: { value: "#rantID#", type: "cf_sql_numeric" } }, 82 | { returntype: "array" } 83 | ).len() 84 | ) 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v2/models/UserService.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * I am the User Service V2 3 | */ 4 | component singleton accessors="true" { 5 | 6 | /** 7 | * Constructor 8 | */ 9 | UserService function init() { 10 | return this; 11 | } 12 | 13 | function get( required numeric userID ) { 14 | return queryExecute( 15 | "select * from users 16 | where id = :userID", 17 | { userID: { value: "#userID#", type: "cf_sql_numeric" } }, 18 | { returntype: "array" } 19 | ); 20 | } 21 | 22 | boolean function exists( required numeric userID ) { 23 | return booleanFormat( 24 | queryExecute( 25 | "select id from users 26 | where id = :userID", 27 | { userID: { value: "#userID#", type: "cf_sql_numeric" } }, 28 | { returntype: "array" } 29 | ).len() 30 | ) 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v3/ModuleConfig.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | Module Directives as public properties 3 | this.title = "Title of the module"; 4 | this.author = "Author of the module"; 5 | this.webURL = "Web URL for docs purposes"; 6 | this.description = "Module description"; 7 | this.version = "Module Version"; 8 | this.viewParentLookup = (true) [boolean] (Optional) // If true, checks for views in the parent first, then it the module.If false, then modules first, then parent. 9 | this.layoutParentLookup = (true) [boolean] (Optional) // If true, checks for layouts in the parent first, then it the module.If false, then modules first, then parent. 10 | this.entryPoint = "" (Optional) // If set, this is the default event (ex:forgebox:manager.index) or default route (/forgebox) the framework 11 | will use to create an entry link to the module. Similar to a default event. 12 | this.cfmapping = "The CF mapping to create"; 13 | this.modelNamespace = "The namespace to use for registered models, if blank it uses the name of the module." 14 | this.dependencies = "The array of dependencies for this module" 15 | 16 | structures to create for configuration 17 | - parentSettings : struct (will append and override parent) 18 | - settings : struct 19 | - interceptorSettings : struct of the following keys ATM 20 | - customInterceptionPoints : string list of custom interception points 21 | - interceptors : array 22 | - layoutSettings : struct (will allow to define a defaultLayout for the module) 23 | - routes : array Allowed keys are same as the addRoute() method of the SES interceptor. 24 | - wirebox : The wirebox DSL to load and use 25 | 26 | Available objects in variable scope 27 | - controller 28 | - appMapping (application mapping) 29 | - moduleMapping (include,cf path) 30 | - modulePath (absolute path) 31 | - log (A pre-configured logBox logger object for this object) 32 | - binder (The wirebox configuration binder) 33 | - wirebox (The wirebox injector) 34 | 35 | Required Methods 36 | - configure() : The method ColdBox calls to configure the module. 37 | 38 | Optional Methods 39 | - onLoad() : If found, it is fired once the module is fully loaded 40 | - onUnload() : If found, it is fired once the module is unloaded 41 | 42 | */ 43 | component { 44 | 45 | // Module Properties 46 | this.title = "v3"; 47 | this.author = ""; 48 | this.webURL = ""; 49 | this.description = ""; 50 | this.version = "1.0.0"; 51 | // If true, looks for views in the parent first, if not found, then in the module. Else vice-versa 52 | this.viewParentLookup = true; 53 | // If true, looks for layouts in the parent first, if not found, then in module. Else vice-versa 54 | this.layoutParentLookup = true; 55 | // Module Entry Point 56 | this.entryPoint = "v3"; 57 | // Inherit entry point from parent, so this will be /api/v1 58 | this.inheritEntryPoint = true; 59 | // Model Namespace 60 | this.modelNamespace = "v3"; 61 | // CF Mapping 62 | this.cfmapping = "v3"; 63 | // Auto-map models 64 | this.autoMapModels = true; 65 | // Module Dependencies 66 | this.dependencies = []; 67 | 68 | function configure() { 69 | // parent settings 70 | parentSettings = {}; 71 | 72 | // module settings - stored in modules.name.settings 73 | settings = {}; 74 | 75 | // Layout Settings 76 | layoutSettings = { defaultLayout: "" }; 77 | 78 | // SES Routes: config/Router.cfc 79 | 80 | // Custom Declared Points 81 | interceptorSettings = { customInterceptionPoints: "" }; 82 | 83 | // Custom Declared Interceptors 84 | interceptors = []; 85 | 86 | // Binder Mappings 87 | // binder.map("Alias").to("#moduleMapping#.model.MyService"); 88 | } 89 | 90 | /** 91 | * Fired when the module is registered and activated. 92 | */ 93 | function onLoad() { 94 | } 95 | 96 | /** 97 | * Fired when the module is unregistered and unloaded 98 | */ 99 | function onUnload() { 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v3/config/Router.cfc: -------------------------------------------------------------------------------- 1 | component { 2 | 3 | function configure() { 4 | resources( resource = "rants", parameterName = "rantID", except = [ "new", "edit" ] ); 5 | 6 | route( "/", "echo.index" ); 7 | route( "/:handler/:action" ).end(); 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v3/handlers/Echo.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * My RESTFul Event Handler which inherits from the module `api` 3 | */ 4 | component extends="api.handlers.BaseHandler" { 5 | 6 | // OPTIONAL HANDLER PROPERTIES 7 | this.prehandler_only = ""; 8 | this.prehandler_except = ""; 9 | this.posthandler_only = ""; 10 | this.posthandler_except = ""; 11 | this.aroundHandler_only = ""; 12 | this.aroundHandler_except = ""; 13 | 14 | // REST Allowed HTTP Methods Ex: this.allowedMethods = {delete='POST,DELETE',index='GET'} 15 | this.allowedMethods = {}; 16 | 17 | /** 18 | * Index 19 | */ 20 | any function index( event, rc, prc ) { 21 | prc.response.setData( "Welcome to my ColdBox RESTFul Service v3" ); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v3/handlers/Rants.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * My RESTFul Rants Event Handler which inherits from the module `api` 3 | */ 4 | component extends="api.handlers.BaseHandler" { 5 | 6 | // DI 7 | property name="rantService" inject="RantService@v3"; 8 | property name="userService" inject="UserService@v3"; 9 | 10 | 11 | /** 12 | * Returns a list of Rants 13 | */ 14 | any function index( event, rc, prc ) { 15 | prc.response.setData( rantService.list() ); 16 | } 17 | 18 | /** 19 | * Returns a single Rant 20 | * 21 | */ 22 | function show( event, rc, prc ) { 23 | var validationResults = validateOrFail( 24 | target = rc, 25 | constraints = { rantID: { required: true, type: "numeric" } } 26 | ); 27 | prc.response.setData( 28 | deserializeJSON( serializeJSON( rantService.getOrFail( rc.rantID )[ 1 ], "struct" ) ) 29 | ) 30 | } 31 | 32 | /** 33 | * Deletes a single Rant 34 | * 35 | */ 36 | function delete( event, rc, prc ) { 37 | var validationResults = validateOrFail( 38 | target = rc, 39 | constraints = { rantID: { required: true, type: "numeric" } } 40 | ); 41 | rantService.existsOrFail( rc.rantID ) 42 | var result = rantService.delete( rc.rantID ); 43 | prc.response.addMessage( "Rant deleted" ); 44 | } 45 | 46 | /** 47 | * Creates a new Rant 48 | * 49 | */ 50 | function create( event, rc, prc ) { 51 | var validationResults = validateOrFail( 52 | target = rc, 53 | constraints = { userID: { required: true, type: "numeric" }, body: { required: true } } 54 | ); 55 | userService.existsOrFail( rc.userID ); 56 | var result = rantService.create( body = rc.body, userID = rc.userID ); 57 | if ( result.recordcount ) { 58 | prc.response.setData( { "rantID": result.generatedKey } ); 59 | prc.response.addMessage( "Rant created" ); 60 | return; 61 | } 62 | } 63 | 64 | /** 65 | * Updates an Existing Rant 66 | * 67 | */ 68 | function update( event, rc, prc ) { 69 | var validationResults = validateOrFail( 70 | target = rc, 71 | constraints = { 72 | rantID: { required: true, type: "numeric" }, 73 | body: { required: true }, 74 | userID: { required: true, type: "numeric" } 75 | } 76 | ); 77 | 78 | rantService.existsOrFail( rc.rantID ); 79 | userService.existsOrFail( rc.userID ); 80 | 81 | var result = rantService.update( body = rc.body, userID = rc.userID, rantID = rc.rantID ); 82 | if ( result.recordcount ) { 83 | prc.response.addMessage( "Rant Updated" ); 84 | } 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v3/models/BaseService.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * I am the Base Service 3 | */ 4 | component accessors="true" { 5 | 6 | property name="entityName"; 7 | property name="tableName"; 8 | property name="primaryKey"; 9 | // property name="parameterName"; 10 | property name="serviceName"; 11 | property name="moduleName"; 12 | 13 | function init( 14 | entityName, 15 | tableName, 16 | primaryKey = "id", 17 | // parameterName="", 18 | serviceName = "", 19 | moduleName = "" 20 | ) { 21 | setEntityName( arguments.entityName ); 22 | setTableName( arguments.tableName ); 23 | setPrimaryKey( arguments.primaryKey ); 24 | // if( arguments.parameterName != "" ){ 25 | // setParameterName( arguments.parameterName ); 26 | // } else { 27 | // setParameterName( arguments.primaryKey ); 28 | // } 29 | if ( arguments.serviceName != "" ) { 30 | setServiceName( arguments.serviceName ); 31 | } else { 32 | setServiceName( arguments.entityName & "Service" ); 33 | } 34 | setModuleName( arguments.moduleName ); 35 | } 36 | 37 | /** 38 | * Check to see if there is a row with a matching primary key in the database. Much faster than a full entity query and object load 39 | * 40 | * @return Returns true if there is a row with the matching Primary Key, otherwise returns false 41 | */ 42 | boolean function exists() { 43 | return booleanFormat( 44 | queryExecute( 45 | "select id from #getTableName()# 46 | where #getPrimaryKey()# = :id", 47 | { id: { value: arguments[ 1 ], type: "cf_sql_numeric" } }, 48 | { returntype: "array" } 49 | ).len() 50 | ) 51 | } 52 | 53 | /** 54 | * Check to see if there is a row with a matching primary key in the database. Much faster than a full entity query and object load 55 | * 56 | * @return Returns true if there is a row with the matching Primary Key 57 | * @throws EntityNotFound if the entity is not found 58 | */ 59 | function existsOrFail() { 60 | if ( exists( argumentCollection = arguments ) ) { 61 | return true; 62 | } else { 63 | throw( type = "EntityNotFound", message = "#entityName# Not Found" ); 64 | } 65 | } 66 | 67 | /** 68 | * Query and load an entity if possible, else throw an error 69 | * 70 | * @return Returns the Entity if there is a row with the matching Primary Key 71 | * @throws EntityNotFound if the entity is not found 72 | */ 73 | function getOrFail() { 74 | var maybeEntity = this.get( argumentCollection = arguments ); 75 | if ( !maybeEntity.len() ) { 76 | throw( type = "EntityNotFound", message = "#getEntityName()# Not Found" ); 77 | } 78 | return maybeEntity; 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v3/models/RantService.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * I am the Rant Service v3 3 | */ 4 | component extends="v3.models.BaseService" singleton accessors="true" { 5 | 6 | /** 7 | * Constructor 8 | */ 9 | RantService function init() { 10 | super.init( 11 | entityName = "rant", 12 | tableName = "rants", 13 | parameterName = "rantID", 14 | moduleName = "v3" 15 | ) 16 | return this; 17 | } 18 | 19 | function list() { 20 | return queryExecute( "select * from rants ORDER BY createdDate DESC", {}, { returntype: "array" } ); 21 | } 22 | 23 | function get( required numeric rantID ) { 24 | return queryExecute( 25 | "select * from rants 26 | where id = :rantID", 27 | { rantID: { value: "#rantID#", type: "cf_sql_numeric" } }, 28 | { returntype: "array" } 29 | ); 30 | } 31 | 32 | function delete( required numeric rantID ) { 33 | queryExecute( 34 | "delete from rants 35 | where id = :rantID", 36 | { rantID: { value: "#rantID#", type: "cf_sql_numeric" } }, 37 | { result: "local.result" } 38 | ); 39 | return local.result; 40 | } 41 | 42 | function create( required body, required numeric userID ) { 43 | var now = now(); 44 | queryExecute( 45 | "insert into rants 46 | set 47 | body = :body, 48 | userID = :userID, 49 | createdDate = :createdDate, 50 | modifiedDate = :modifiedDate 51 | ", 52 | { 53 | body: { value: "#body#", type: "cf_sql_longvarchar" }, 54 | userID: { value: "#userID#", type: "cf_sql_numeric" }, 55 | createdDate: { value: "#now#", type: "cf_sql_timestamp" }, 56 | modifiedDate: { value: "#now#", type: "cf_sql_timestamp" } 57 | }, 58 | { result: "local.result" } 59 | ); 60 | return local.result; 61 | } 62 | 63 | function update( required body, ) { 64 | var now = now(); 65 | queryExecute( 66 | "update rants 67 | set 68 | body = :body, 69 | modifiedDate = :modifiedDate 70 | where id = :rantID 71 | ", 72 | { 73 | rantID: { value: "#rantID#", type: "cf_sql_integer" }, 74 | body: { value: "#body#", type: "cf_sql_longvarchar" }, 75 | modifiedDate: { value: "#now#", type: "cf_sql_timestamp" } 76 | }, 77 | { result: "local.result" } 78 | ); 79 | return local.result; 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v3/models/UserService.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * I am the User Service v3 3 | */ 4 | component extends="v3.models.BaseService" singleton accessors="true" { 5 | 6 | /** 7 | * Constructor 8 | */ 9 | UserService function init() { 10 | super.init( 11 | entityName = "user", 12 | tableName = "users", 13 | parameterName = "userID", 14 | moduleName = "v3" 15 | ) 16 | return this; 17 | } 18 | 19 | function get( required numeric userID ) { 20 | return queryExecute( 21 | "select * from users 22 | where id = :userID", 23 | { userID: { value: "#userID#", type: "cf_sql_numeric" } }, 24 | { returntype: "array" } 25 | ); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v4/ModuleConfig.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | Module Directives as public properties 3 | this.title = "Title of the module"; 4 | this.author = "Author of the module"; 5 | this.webURL = "Web URL for docs purposes"; 6 | this.description = "Module description"; 7 | this.version = "Module Version"; 8 | this.viewParentLookup = (true) [boolean] (Optional) // If true, checks for views in the parent first, then it the module.If false, then modules first, then parent. 9 | this.layoutParentLookup = (true) [boolean] (Optional) // If true, checks for layouts in the parent first, then it the module.If false, then modules first, then parent. 10 | this.entryPoint = "" (Optional) // If set, this is the default event (ex:forgebox:manager.index) or default route (/forgebox) the framework 11 | will use to create an entry link to the module. Similar to a default event. 12 | this.cfmapping = "The CF mapping to create"; 13 | this.modelNamespace = "The namespace to use for registered models, if blank it uses the name of the module." 14 | this.dependencies = "The array of dependencies for this module" 15 | 16 | structures to create for configuration 17 | - parentSettings : struct (will append and override parent) 18 | - settings : struct 19 | - interceptorSettings : struct of the following keys ATM 20 | - customInterceptionPoints : string list of custom interception points 21 | - interceptors : array 22 | - layoutSettings : struct (will allow to define a defaultLayout for the module) 23 | - routes : array Allowed keys are same as the addRoute() method of the SES interceptor. 24 | - wirebox : The wirebox DSL to load and use 25 | 26 | Available objects in variable scope 27 | - controller 28 | - appMapping (application mapping) 29 | - moduleMapping (include,cf path) 30 | - modulePath (absolute path) 31 | - log (A pre-configured logBox logger object for this object) 32 | - binder (The wirebox configuration binder) 33 | - wirebox (The wirebox injector) 34 | 35 | Required Methods 36 | - configure() : The method ColdBox calls to configure the module. 37 | 38 | Optional Methods 39 | - onLoad() : If found, it is fired once the module is fully loaded 40 | - onUnload() : If found, it is fired once the module is unloaded 41 | 42 | */ 43 | component { 44 | 45 | // Module Properties 46 | this.title = "v4"; 47 | this.author = ""; 48 | this.webURL = ""; 49 | this.description = ""; 50 | this.version = "1.0.0"; 51 | // If true, looks for views in the parent first, if not found, then in the module. Else vice-versa 52 | this.viewParentLookup = true; 53 | // If true, looks for layouts in the parent first, if not found, then in module. Else vice-versa 54 | this.layoutParentLookup = true; 55 | // Module Entry Point 56 | this.entryPoint = "v4"; 57 | // Inherit entry point from parent, so this will be /api/v1 58 | this.inheritEntryPoint = true; 59 | // Model Namespace 60 | this.modelNamespace = "v4"; 61 | // CF Mapping 62 | this.cfmapping = "v4"; 63 | // Auto-map models 64 | this.autoMapModels = true; 65 | // Module Dependencies 66 | this.dependencies = []; 67 | 68 | function configure() { 69 | // parent settings 70 | parentSettings = {}; 71 | 72 | // module settings - stored in modules.name.settings 73 | settings = {}; 74 | 75 | // Layout Settings 76 | layoutSettings = { defaultLayout: "" }; 77 | 78 | // SES Routes: config/Router.cfc 79 | 80 | // Custom Declared Points 81 | interceptorSettings = { customInterceptionPoints: "" }; 82 | 83 | // Custom Declared Interceptors 84 | interceptors = []; 85 | 86 | // Binder Mappings 87 | // binder.map("Alias").to("#moduleMapping#.model.MyService"); 88 | } 89 | 90 | /** 91 | * Fired when the module is registered and activated. 92 | */ 93 | function onLoad() { 94 | } 95 | 96 | /** 97 | * Fired when the module is unregistered and unloaded 98 | */ 99 | function onUnload() { 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v4/config/Router.cfc: -------------------------------------------------------------------------------- 1 | component { 2 | 3 | function configure() { 4 | resources( resource = "rants", parameterName = "rantID", except = [ "new", "edit" ] ); 5 | 6 | route( "/", "echo.index" ); 7 | route( "/:handler/:action" ).end(); 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v4/handlers/Echo.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * My RESTFul Event Handler which inherits from the module `api` 3 | */ 4 | component extends="api.handlers.BaseHandler" { 5 | 6 | // OPTIONAL HANDLER PROPERTIES 7 | this.prehandler_only = ""; 8 | this.prehandler_except = ""; 9 | this.posthandler_only = ""; 10 | this.posthandler_except = ""; 11 | this.aroundHandler_only = ""; 12 | this.aroundHandler_except = ""; 13 | 14 | // REST Allowed HTTP Methods Ex: this.allowedMethods = {delete='POST,DELETE',index='GET'} 15 | this.allowedMethods = {}; 16 | 17 | /** 18 | * Index 19 | */ 20 | any function index( event, rc, prc ) { 21 | prc.response.setData( "Welcome to my ColdBox RESTFul Service v4" ); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v4/handlers/Rants.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * My RESTFul Rants Event Handler which inherits from the module `api` 3 | */ 4 | component extends="api.handlers.BaseHandler" { 5 | 6 | // DI 7 | property name="rantService" inject="RantService@v4"; 8 | property name="userService" inject="UserService@v4"; 9 | 10 | 11 | /** 12 | * Returns a list of Rants 13 | */ 14 | any function index( event, rc, prc ) { 15 | prc.response.setData( rantService.listArray() ); 16 | // prc.response.setData( 17 | // rantService.list().map( function( rant ) { 18 | // return rant.getMemento(); 19 | // }) 20 | // ); 21 | } 22 | 23 | /** 24 | * Returns a single Rant 25 | * 26 | */ 27 | function show( event, rc, prc ) { 28 | var validationResults = validateOrFail( 29 | target = rc, 30 | constraints = { rantID: { required: true, type: "numeric" } } 31 | ); 32 | prc.response.setData( rantService.getOrFail( rc.rantID ).getMemento() ) 33 | } 34 | 35 | /** 36 | * Deletes a single Rant 37 | * 38 | */ 39 | function delete( event, rc, prc ) { 40 | var validationResults = validateOrFail( 41 | target = rc, 42 | constraints = { rantID: { required: true, type: "numeric" } } 43 | ); 44 | rantService.existsOrFail( rc.rantID ) 45 | var result = rantService.delete( rc.rantID ); 46 | prc.response.addMessage( "Rant deleted" ); 47 | } 48 | 49 | /** 50 | * Creates a new Rant 51 | * 52 | */ 53 | function create( event, rc, prc ) { 54 | var validationResults = validateOrFail( 55 | target = rc, 56 | constraints = { userID: { required: true, type: "numeric" }, body: { required: true } } 57 | ); 58 | userService.existsOrFail( rc.userID ); 59 | var rant = rantService.new(); 60 | rant.setBody( rc.body ); 61 | rant.setUserID( rc.userID ); 62 | // var rant = populateModel( rantService.new() ); 63 | validate( 64 | target = rant, 65 | constraints = { body: { required: true }, userID: { required: true, type: "numeric" } } 66 | ); 67 | 68 | var result = rantService.create( rant ); 69 | prc.response.setData( { "rantID": result.getID() } ); 70 | prc.response.addMessage( "Rant created" ); 71 | } 72 | 73 | /** 74 | * Updates an Existing Rant 75 | * 76 | */ 77 | function update( event, rc, prc ) { 78 | var validationResults = validateOrFail( 79 | target = rc, 80 | constraints = { 81 | rantID: { required: true, type: "numeric" }, 82 | body: { required: true }, 83 | userID: { required: true, type: "numeric" } 84 | } 85 | ); 86 | 87 | rantService.existsOrFail( rc.rantID ); 88 | userService.existsOrFail( rc.userID ); 89 | 90 | var rant = rantService.getOrFail( rc.rantID ); 91 | // rant.setBody( rc.body ); 92 | // rant.setUserID( rc.userID ); 93 | // rant.setID( rc.rantID ) 94 | 95 | var rant = populateModel( model = rantService.new() ); 96 | rant.setID( rc.rantID ) 97 | validate( 98 | target = rant, 99 | constraints = { 100 | id: { required: true, type: "numeric" }, 101 | body: { required: true }, 102 | userID: { required: true, type: "numeric" } 103 | } 104 | ); 105 | var result = rantService.update( rant ); 106 | prc.response.addMessage( "Rant Updated" ); 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v4/models/BaseService.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * I am the Base Service 3 | */ 4 | component accessors="true" { 5 | 6 | property name="entityName"; 7 | property name="tableName"; 8 | property name="primaryKey"; 9 | // property name="parameterName"; 10 | property name="serviceName"; 11 | property name="moduleName"; 12 | 13 | function init( 14 | entityName, 15 | tableName, 16 | primaryKey = "id", 17 | // parameterName="", 18 | serviceName = "", 19 | moduleName = "" 20 | ) { 21 | setEntityName( arguments.entityName ); 22 | setTableName( arguments.tableName ); 23 | setPrimaryKey( arguments.primaryKey ); 24 | // if( arguments.parameterName != "" ){ 25 | // setParameterName( arguments.parameterName ); 26 | // } else { 27 | // setParameterName( arguments.primaryKey ); 28 | // } 29 | if ( arguments.serviceName != "" ) { 30 | setServiceName( arguments.serviceName ); 31 | } else { 32 | setServiceName( arguments.entityName & "Service" ); 33 | } 34 | setModuleName( arguments.moduleName ); 35 | } 36 | 37 | /** 38 | * Check to see if there is a row with a matching primary key in the database. Much faster than a full entity query and object load 39 | * 40 | * @return Returns true if there is a row with the matching Primary Key, otherwise returns false 41 | */ 42 | boolean function exists() { 43 | return booleanFormat( 44 | queryExecute( 45 | "select id from #getTableName()# 46 | where #getPrimaryKey()# = :id", 47 | { id: { value: arguments[ 1 ], type: "cf_sql_numeric" } }, 48 | { returntype: "array" } 49 | ).len() 50 | ) 51 | } 52 | 53 | /** 54 | * Check to see if there is a row with a matching primary key in the database. Much faster than a full entity query and object load 55 | * 56 | * @return Returns true if there is a row with the matching Primary Key 57 | * @throws EntityNotFound if the entity is not found 58 | */ 59 | function existsOrFail() { 60 | if ( exists( argumentCollection = arguments ) ) { 61 | return true; 62 | } else { 63 | throw( type = "EntityNotFound", message = "#entityName# Not Found" ); 64 | } 65 | } 66 | 67 | /** 68 | * Query and load an entity if possible, else throw an error 69 | * 70 | * @return Returns the Entity if there is a row with the matching Primary Key 71 | * @throws EntityNotFound if the entity is not found 72 | */ 73 | function getOrFail() { 74 | var maybeEntity = this.get( argumentCollection = arguments ); 75 | if ( isNull( maybeEntity ) || !maybeEntity.isLoaded() ) { 76 | throw( type = "EntityNotFound", message = "#getEntityName()# Not Found" ); 77 | } 78 | return maybeEntity; 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v4/models/Rant.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * I am a new Rant Object 3 | */ 4 | component accessors="true" { 5 | 6 | // DI 7 | property name="userService" inject="UserService@v4"; 8 | 9 | // Properties 10 | property name="id" type="string"; 11 | property name="body" type="string"; 12 | property name="createdDate" type="date"; 13 | property name="modifiedDate" type="date"; 14 | property name="userID" type="string"; 15 | 16 | 17 | /** 18 | * Constructor 19 | */ 20 | Rant function init() { 21 | return this; 22 | } 23 | 24 | /** 25 | * getUser 26 | */ 27 | function getUser() { 28 | return userService.get( getUserID() ); 29 | } 30 | 31 | /** 32 | * isLoaded 33 | */ 34 | boolean function isLoaded() { 35 | return ( !isNull( variables.id ) && len( variables.id ) ); 36 | } 37 | 38 | function getMemento() { 39 | return { 40 | "id": getID(), 41 | "body": getBody(), 42 | "createdDate": dateFormat( getCreatedDate(), "long" ), 43 | "modifiedDate": dateFormat( getModifiedDate(), "long" ), 44 | "userId": getUserID() 45 | }; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v4/models/RantService.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * I am the Rant Service v4 3 | */ 4 | component extends="v4.models.BaseService" singleton accessors="true" { 5 | 6 | // To populate objects from data 7 | property name="populator" inject="wirebox:populator"; 8 | 9 | /** 10 | * Constructor 11 | */ 12 | RantService function init() { 13 | super.init( 14 | entityName = "rant", 15 | tableName = "rants", 16 | parameterName = "rantID", 17 | moduleName = "v4" 18 | ) 19 | return this; 20 | } 21 | 22 | Rant function new() provider="Rant@v4" { 23 | } 24 | 25 | function list() { 26 | return this 27 | .listArray() 28 | .map( function( rant ) { 29 | return populator.populateFromStruct( new (), rant ); 30 | } ); 31 | } 32 | 33 | function listArray() { 34 | return queryExecute( "select * from rants ORDER BY createdDate DESC", {}, { returntype: "array" } ) 35 | } 36 | 37 | Rant function get( required numeric rantID ) { 38 | var q = queryExecute( 39 | "select * from rants 40 | where id = :rantID", 41 | { rantID: { value: "#rantID#", type: "cf_sql_numeric" } }, 42 | { returntype: "array" } 43 | ); 44 | if ( q.len() ) { 45 | return populator.populateFromStruct( new (), q[ 1 ] ); 46 | } else { 47 | return new () 48 | } 49 | } 50 | 51 | function delete( required numeric rantID ) { 52 | queryExecute( 53 | "delete from rants 54 | where id = :rantID", 55 | { rantID: { value: "#rantID#", type: "cf_sql_numeric" } }, 56 | { result: "local.result" } 57 | ); 58 | return local.result; 59 | } 60 | 61 | function create( required Rant rant ) { 62 | var now = now(); 63 | arguments.rant.setCreatedDate( now ); 64 | arguments.rant.setModifiedDate( now ); 65 | 66 | queryExecute( 67 | "insert into rants 68 | set 69 | body = :body, 70 | userID = :userID, 71 | createdDate = :createdDate, 72 | modifiedDate = :modifiedDate 73 | ", 74 | { 75 | body: { value: "#arguments.rant.getBody()#", type: "cf_sql_longvarchar" }, 76 | userID: { value: "#arguments.rant.getuserID()#", type: "cf_sql_numeric" }, 77 | createdDate: { value: "#arguments.rant.getCreatedDate()#", type: "cf_sql_timestamp" }, 78 | modifiedDate: { value: "#arguments.rant.getModifiedDate()#", type: "cf_sql_timestamp" } 79 | }, 80 | { result: "local.result" } 81 | ); 82 | arguments.rant.setID( local.result.generatedKey ); 83 | return arguments.rant; 84 | } 85 | 86 | function update( required Rant rant ) { 87 | var now = now(); 88 | arguments.rant.setModifiedDate( now ); 89 | queryExecute( 90 | "update rants 91 | set 92 | body = :body, 93 | modifiedDate = :modifiedDate 94 | where id = :rantID 95 | ", 96 | { 97 | rantID: { value: "#arguments.rant.getID()#", type: "cf_sql_integer" }, 98 | body: { value: "#arguments.rant.getBody()#", type: "cf_sql_longvarchar" }, 99 | modifiedDate: { value: "#arguments.rant.getModifiedDate()#", type: "cf_sql_timestamp" } 100 | }, 101 | { result: "local.result" } 102 | ); 103 | return local.result; 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v4/models/UserService.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * I am the User Service V4 3 | */ 4 | component extends="v4.models.BaseService" singleton accessors="true" { 5 | 6 | /** 7 | * Constructor 8 | */ 9 | UserService function init() { 10 | super.init( 11 | entityName = "user", 12 | tableName = "users", 13 | parameterName = "userID", 14 | moduleName = "v4" 15 | ) 16 | return this; 17 | } 18 | 19 | User function get( required numeric userID ) { 20 | var q = queryExecute( 21 | "select * from users 22 | where id = :userID", 23 | { userID: { value: "#userID#", type: "cf_sql_numeric" } }, 24 | { returntype: "array" } 25 | ); 26 | if ( q.len() ) { 27 | return populator.populateFromStruct( new (), q[ 1 ] ); 28 | } else { 29 | return new () 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v5/ModuleConfig.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | Module Directives as public properties 3 | this.title = "Title of the module"; 4 | this.author = "Author of the module"; 5 | this.webURL = "Web URL for docs purposes"; 6 | this.description = "Module description"; 7 | this.version = "Module Version"; 8 | this.viewParentLookup = (true) [boolean] (Optional) // If true, checks for views in the parent first, then it the module.If false, then modules first, then parent. 9 | this.layoutParentLookup = (true) [boolean] (Optional) // If true, checks for layouts in the parent first, then it the module.If false, then modules first, then parent. 10 | this.entryPoint = "" (Optional) // If set, this is the default event (ex:forgebox:manager.index) or default route (/forgebox) the framework 11 | will use to create an entry link to the module. Similar to a default event. 12 | this.cfmapping = "The CF mapping to create"; 13 | this.modelNamespace = "The namespace to use for registered models, if blank it uses the name of the module." 14 | this.dependencies = "The array of dependencies for this module" 15 | 16 | structures to create for configuration 17 | - parentSettings : struct (will append and override parent) 18 | - settings : struct 19 | - interceptorSettings : struct of the following keys ATM 20 | - customInterceptionPoints : string list of custom interception points 21 | - interceptors : array 22 | - layoutSettings : struct (will allow to define a defaultLayout for the module) 23 | - routes : array Allowed keys are same as the addRoute() method of the SES interceptor. 24 | - wirebox : The wirebox DSL to load and use 25 | 26 | Available objects in variable scope 27 | - controller 28 | - appMapping (application mapping) 29 | - moduleMapping (include,cf path) 30 | - modulePath (absolute path) 31 | - log (A pre-configured logBox logger object for this object) 32 | - binder (The wirebox configuration binder) 33 | - wirebox (The wirebox injector) 34 | 35 | Required Methods 36 | - configure() : The method ColdBox calls to configure the module. 37 | 38 | Optional Methods 39 | - onLoad() : If found, it is fired once the module is fully loaded 40 | - onUnload() : If found, it is fired once the module is unloaded 41 | 42 | */ 43 | component { 44 | 45 | // Module Properties 46 | this.title = "v5"; 47 | this.author = ""; 48 | this.webURL = ""; 49 | this.description = ""; 50 | this.version = "1.0.0"; 51 | // If true, looks for views in the parent first, if not found, then in the module. Else vice-versa 52 | this.viewParentLookup = true; 53 | // If true, looks for layouts in the parent first, if not found, then in module. Else vice-versa 54 | this.layoutParentLookup = true; 55 | // Module Entry Point 56 | this.entryPoint = "v5"; 57 | // Inherit entry point from parent, so this will be /api/v1 58 | this.inheritEntryPoint = true; 59 | // Model Namespace 60 | this.modelNamespace = "v5"; 61 | // CF Mapping 62 | this.cfmapping = "v5"; 63 | // Auto-map models 64 | this.autoMapModels = true; 65 | // Module Dependencies 66 | this.dependencies = []; 67 | 68 | function configure() { 69 | // parent settings 70 | parentSettings = {}; 71 | 72 | // module settings - stored in modules.name.settings 73 | settings = {}; 74 | 75 | // Layout Settings 76 | layoutSettings = { defaultLayout: "" }; 77 | 78 | // SES Routes: config/Router.cfc 79 | 80 | // Custom Declared Points 81 | interceptorSettings = { customInterceptionPoints: "" }; 82 | 83 | // Custom Declared Interceptors 84 | interceptors = []; 85 | 86 | // Binder Mappings 87 | // binder.map("Alias").to("#moduleMapping#.model.MyService"); 88 | } 89 | 90 | /** 91 | * Fired when the module is registered and activated. 92 | */ 93 | function onLoad() { 94 | } 95 | 96 | /** 97 | * Fired when the module is unregistered and unloaded 98 | */ 99 | function onUnload() { 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v5/config/Router.cfc: -------------------------------------------------------------------------------- 1 | component { 2 | 3 | function configure() { 4 | resources( resource = "rants", parameterName = "rantID", except = [ "new", "edit" ] ); 5 | 6 | route( "/", "echo.index" ); 7 | route( "/:handler/:action" ).end(); 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v5/handlers/Echo.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * My RESTFul Event Handler which inherits from the module `api` 3 | */ 4 | component extends="api.handlers.BaseHandler" { 5 | 6 | // OPTIONAL HANDLER PROPERTIES 7 | this.prehandler_only = ""; 8 | this.prehandler_except = ""; 9 | this.posthandler_only = ""; 10 | this.posthandler_except = ""; 11 | this.aroundHandler_only = ""; 12 | this.aroundHandler_except = ""; 13 | 14 | // REST Allowed HTTP Methods Ex: this.allowedMethods = {delete='POST,DELETE',index='GET'} 15 | this.allowedMethods = {}; 16 | 17 | /** 18 | * Index 19 | */ 20 | any function index( event, rc, prc ) { 21 | prc.response.setData( "Welcome to my ColdBox RESTFul Service v5" ); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v5/handlers/Rants.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * My RESTFul Rants Event Handler which inherits from the module `api` 3 | */ 4 | component extends="api.handlers.BaseHandler" { 5 | 6 | // DI 7 | property name="rantService" inject="RantService@v5"; 8 | property name="userService" inject="UserService@v5"; 9 | 10 | 11 | /** 12 | * Returns a list of Rants 13 | */ 14 | any function index( event, rc, prc ) { 15 | prc.response.setData( rantService.listArray() ); 16 | } 17 | 18 | /** 19 | * Returns a single Rant 20 | * 21 | */ 22 | function show( event, rc, prc ) { 23 | var validationResults = validateOrFail( 24 | target = rc, 25 | constraints = { rantID: { required: true, type: "numeric" } } 26 | ); 27 | prc.response.setData( rantService.getOrFail( rc.rantID ).getMemento() ) 28 | } 29 | 30 | /** 31 | * Deletes a single Rant 32 | * 33 | */ 34 | function delete( event, rc, prc ) { 35 | var validationResults = validateOrFail( 36 | target = rc, 37 | constraints = { rantID: { required: true, type: "numeric" } } 38 | ); 39 | rantService.getOrFail( rc.rantID ).delete(); 40 | prc.response.addMessage( "Rant deleted" ); 41 | } 42 | 43 | /** 44 | * Creates a new Rant 45 | * 46 | */ 47 | function create( event, rc, prc ) { 48 | var validationResults = validateOrFail( 49 | target = rc, 50 | constraints = { userID: { required: true, type: "numeric" }, body: { required: true } } 51 | ); 52 | userService.existsOrFail( rc.userID ); 53 | var result = rantService 54 | .new( validationResults ) 55 | .validateOrFail( { body: { required: true }, userID: { required: true, type: "numeric" } } ) 56 | .save(); 57 | prc.response.setData( { "rantID": result.getID() } ); 58 | prc.response.addMessage( "Rant created" ); 59 | } 60 | 61 | /** 62 | * Updates an Existing Rant 63 | * 64 | */ 65 | function update( event, rc, prc ) { 66 | var validationResults = validateOrFail( 67 | target = rc, 68 | constraints = { 69 | rantID: { required: true, type: "numeric" }, 70 | body: { required: true }, 71 | userID: { required: true, type: "numeric" } 72 | } 73 | ); 74 | userService.existsOrFail( rc.userID ); 75 | rantService 76 | .getOrFail( rc.rantID ) 77 | .populate( validationResults ) 78 | .setID( rc.rantID ) 79 | .validateOrFail( 80 | constraints = { 81 | id: { required: true, type: "numeric" }, 82 | body: { required: true }, 83 | userID: { required: true, type: "numeric" } 84 | } 85 | ) 86 | .save(); 87 | prc.response.addMessage( "Rant Updated" ); 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v5/models/BaseEntity.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * I am the Base Entity 3 | */ 4 | component accessors="true" { 5 | 6 | property name="pk"; 7 | property name="entityName"; 8 | property name="serviceName"; 9 | property name="moduleName"; 10 | 11 | // DI Injection 12 | property name="wirebox" inject="wirebox"; 13 | property name="coldbox" inject="coldbox"; 14 | 15 | /** 16 | * Initialize Entity - stores information needed 17 | * 18 | * @pk The name of the primary key field in the entity 19 | * @entityName The name of the entity so we can reference it for calls to related DAO and Service. Set as optional for backwards 20 | */ 21 | function init( 22 | pk = "id", 23 | entityName = "", 24 | serviceName = "", 25 | moduleName = "" 26 | ) { 27 | setPk( arguments.pk ); 28 | if ( len( entityName ) ) { 29 | setEntityName( arguments.entityName ); 30 | } 31 | if ( arguments.serviceName != "" ) { 32 | setServiceName( arguments.serviceName ); 33 | } else { 34 | setServiceName( arguments.entityName & "Service" ); 35 | } 36 | setModuleName( arguments.moduleName ); 37 | if ( arguments.moduleName != "" ) { 38 | setServiceName( getServiceName() & "@" & arguments.moduleName ); 39 | } 40 | } 41 | 42 | /** 43 | * Verify if entity is loaded or not 44 | */ 45 | boolean function isLoaded() { 46 | return ( isNull( variables[ getPk() ] ) OR !len( variables[ getPk() ] ) ? false : true ); 47 | } 48 | 49 | /** 50 | * Populate a model object from the request Collection or a passed in memento structure 51 | * @scope Use scope injection instead of setters population. Ex: scope=variables.instance. 52 | * @trustedSetter If set to true, the setter method will be called even if it does not exist in the object 53 | * @include A list of keys to include in the population 54 | * @exclude A list of keys to exclude in the population 55 | * @ignoreEmpty Ignore empty values on populations, great for ORM population 56 | * @nullEmptyInclude A list of keys to NULL when empty 57 | * @nullEmptyExclude A list of keys to NOT NULL when empty 58 | * @composeRelationships Automatically attempt to compose relationships from memento 59 | * @memento A structure to populate the model, if not passed it defaults to the request collection 60 | * @jsonstring If you pass a json string, we will populate your model with it 61 | * @xml If you pass an xml string, we will populate your model with it 62 | * @qry If you pass a query, we will populate your model with it 63 | * @rowNumber The row of the qry parameter to populate your model with 64 | */ 65 | function populate( 66 | struct memento = coldbox 67 | .getRequestService() 68 | .getContext() 69 | .getCollection(), 70 | scope = "", 71 | boolean trustedSetter = false, 72 | include = "", 73 | exclude = "", 74 | boolean ignoreEmpty = false, 75 | nullEmptyInclude = "", 76 | nullEmptyExclude = "", 77 | boolean composeRelationships = false, 78 | string jsonstring, 79 | string xml, 80 | query qry 81 | ) { 82 | arguments[ "model" ] = this; 83 | arguments.target = this; 84 | 85 | // json? 86 | if ( structKeyExists( arguments, "jsonstring" ) ) { 87 | return wirebox.getObjectPopulator().populateFromJSON( argumentCollection = arguments ); 88 | } 89 | // XML 90 | else if ( structKeyExists( arguments, "xml" ) ) { 91 | return wirebox.getObjectPopulator().populateFromXML( argumentCollection = arguments ); 92 | } 93 | // Query 94 | else if ( structKeyExists( arguments, "qry" ) ) { 95 | return wirebox.getObjectPopulator().populateFromQuery( argumentCollection = arguments ); 96 | } 97 | // Mementos 98 | else { 99 | // populate 100 | return wirebox.getObjectPopulator().populateFromStruct( argumentCollection = arguments ); 101 | } 102 | } 103 | 104 | /** 105 | * Validate an object or structure according to the constraints rules. 106 | * @fields The fields to validate on the target. By default, it validates on all fields 107 | * @constraints A structure of constraint rules or the name of the shared constraint rules to use for validation 108 | * @locale The i18n locale to use for validation messages 109 | * @excludeFields The fields to exclude from the validation 110 | * @includeFields The fields to include in the validation 111 | * 112 | * @return cbvalidation.model.result.IValidationResult 113 | * @throws ValidationException error 114 | */ 115 | public struct function validateOrFail( 116 | any constraints = {}, 117 | string fields = "*", 118 | string locale = "", 119 | string excludeFields = "", 120 | string includeFields = "" 121 | ) { 122 | var result = wirebox 123 | .getInstance( "ValidationManager@cbvalidation" ) 124 | .validate( 125 | target = this, 126 | fields = arguments.fields, 127 | constraints = arguments.constraints, 128 | locale = arguments.locale, 129 | excludeFields = arguments.excludeFields, 130 | includeFields = arguments.includeFields 131 | ); 132 | if ( result.hasErrors() ) { 133 | throw( 134 | type = "ValidationException", 135 | message = "The Model #getEntityName()# failed to pass validation", 136 | extendedInfo = serializeJSON( result.getAllErrors() ) 137 | ); 138 | } 139 | return this; 140 | } 141 | 142 | function save() { 143 | if ( isLoaded() ) { 144 | return wirebox.getInstance( "#getServiceName()#" ).update( this ); 145 | } else { 146 | return wirebox.getInstance( "#getServiceName()#" ).create( this ); 147 | } 148 | } 149 | 150 | function delete() { 151 | return wirebox.getInstance( "#getServiceName()#" ).delete( this.getID() ); 152 | } 153 | 154 | this.memento = { 155 | // An array of the properties/relationships to include by default 156 | defaultIncludes: [ "*" ], 157 | // An array of properties/relationships to exclude by default 158 | defaultExcludes: [], 159 | // An array of properties/relationships to NEVER include 160 | neverInclude: [], 161 | // A struct of defaults for properties/relationships if they are null 162 | defaults: {}, 163 | // A struct of mapping functions for properties/relationships that can transform them 164 | mappers: {} 165 | } 166 | 167 | } 168 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v5/models/BaseService.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * I am the Base Service 3 | */ 4 | component accessors="true" { 5 | 6 | property name="entityName"; 7 | property name="tableName"; 8 | property name="primaryKey"; 9 | // property name="parameterName"; 10 | property name="serviceName"; 11 | property name="moduleName"; 12 | 13 | function init( 14 | entityName, 15 | tableName, 16 | primaryKey = "id", 17 | // parameterName="", 18 | serviceName = "", 19 | moduleName = "" 20 | ) { 21 | setEntityName( arguments.entityName ); 22 | setTableName( arguments.tableName ); 23 | setPrimaryKey( arguments.primaryKey ); 24 | // if( arguments.parameterName != "" ){ 25 | // setParameterName( arguments.parameterName ); 26 | // } else { 27 | // setParameterName( arguments.primaryKey ); 28 | // } 29 | if ( arguments.serviceName != "" ) { 30 | setServiceName( arguments.serviceName ); 31 | } else { 32 | setServiceName( arguments.entityName & "Service" ); 33 | } 34 | setModuleName( arguments.moduleName ); 35 | if ( arguments.moduleName != "" ) { 36 | setServiceName( getServiceName() & "@" & arguments.moduleName ); 37 | } 38 | } 39 | 40 | /** 41 | * Return a new Entity, empty or pre-populated with passed in data 42 | * 43 | * @data Data to populate the new Entity with 44 | */ 45 | function new( struct data = {} ) { 46 | // if( data.isEmpty() ){ 47 | 48 | // } else { 49 | var modelName = getEntityName(); 50 | if ( getModuleName() != "" ) { 51 | modelName = modelName & "@" & getModuleName(); 52 | } 53 | return populator.populateFromStruct( target = wireBox.getInstance( "#modelName#" ), memento = arguments.data ) 54 | // } 55 | } 56 | 57 | /** 58 | * Check to see if there is a row with a matching primary key in the database. Much faster than a full entity query and object load 59 | * 60 | * @return Returns true if there is a row with the matching Primary Key, otherwise returns false 61 | */ 62 | boolean function exists() { 63 | return booleanFormat( 64 | queryExecute( 65 | "select id from #getTableName()# 66 | where #getPrimaryKey()# = :id", 67 | { id: { value: arguments[ 1 ], type: "cf_sql_numeric" } }, 68 | { returntype: "array" } 69 | ).len() 70 | ) 71 | } 72 | 73 | /** 74 | * Check to see if there is a row with a matching primary key in the database. Much faster than a full entity query and object load 75 | * 76 | * @return Returns true if there is a row with the matching Primary Key 77 | * @throws EntityNotFound if the entity is not found 78 | */ 79 | function existsOrFail() { 80 | if ( exists( argumentCollection = arguments ) ) { 81 | return true; 82 | } else { 83 | throw( type = "EntityNotFound", message = "#entityName# Not Found" ); 84 | } 85 | } 86 | 87 | /** 88 | * Query and load an entity if possible, else throw an error 89 | * 90 | * @return Returns the Entity if there is a row with the matching Primary Key 91 | * @throws EntityNotFound if the entity is not found 92 | */ 93 | function getOrFail() { 94 | var maybeEntity = this.get( argumentCollection = arguments ); 95 | if ( isNull( maybeEntity ) || !maybeEntity.isLoaded() ) { 96 | throw( type = "EntityNotFound", message = "#getEntityName()# Not Found" ); 97 | } 98 | return maybeEntity; 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v5/models/Rant.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * I am a new Rant Object 3 | */ 4 | component extends="v5.models.BaseEntity" accessors="true" { 5 | 6 | // DI 7 | property name="userService" inject="UserService@v5"; 8 | 9 | // Properties 10 | property name="id" type="string"; 11 | property name="body" type="string"; 12 | property name="createdDate" type="date"; 13 | property name="modifiedDate" type="date"; 14 | property name="userID" type="string"; 15 | 16 | 17 | /** 18 | * Constructor 19 | */ 20 | Rant function init() { 21 | super.init( entityName = "rant", moduleName = "v5" ); 22 | return this; 23 | } 24 | 25 | /** 26 | * getUser 27 | */ 28 | function getUser() { 29 | return userService.get( getUserID() ); 30 | } 31 | 32 | // function getMemento() { 33 | // return { 34 | // "id" = getID(), 35 | // "body" = getBody(), 36 | // "createdDate" = dateFormat( getCreatedDate(), "long" ), 37 | // "modifiedDate" = dateFormat( getModifiedDate(), "long" ), 38 | // "userId" = getUserID() 39 | // }; 40 | // } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v5/models/RantService.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * I am the Rant Service v5 3 | */ 4 | component extends="v5.models.BaseService" singleton accessors="true" { 5 | 6 | // DI Injection 7 | property name="wirebox" inject="wirebox"; 8 | property name="populator" inject="wirebox:populator"; 9 | 10 | /** 11 | * Constructor 12 | */ 13 | RantService function init() { 14 | super.init( 15 | entityName = "rant", 16 | tableName = "rants", 17 | parameterName = "rantID", 18 | moduleName = "v5" 19 | ) 20 | return this; 21 | } 22 | 23 | function list() { 24 | return this 25 | .listArray() 26 | .map( function( rant ) { 27 | return populator.populateFromStruct( new (), rant ); 28 | } ); 29 | } 30 | 31 | function listArray() { 32 | return queryExecute( "select * from rants ORDER BY createdDate DESC", {}, { returntype: "array" } ) 33 | } 34 | 35 | Rant function get( required numeric rantID ) { 36 | var q = queryExecute( 37 | "select * from rants 38 | where id = :rantID", 39 | { rantID: { value: "#rantID#", type: "cf_sql_numeric" } }, 40 | { returntype: "array" } 41 | ); 42 | if ( q.len() ) { 43 | return populator.populateFromStruct( new (), q[ 1 ] ); 44 | } else { 45 | return new () 46 | } 47 | } 48 | 49 | function delete( required numeric rantID ) { 50 | queryExecute( 51 | "delete from rants 52 | where id = :rantID", 53 | { rantID: { value: "#rantID#", type: "cf_sql_numeric" } }, 54 | { result: "local.result" } 55 | ); 56 | return local.result; 57 | } 58 | 59 | function create( required Rant rant ) { 60 | var now = now(); 61 | arguments.rant.setCreatedDate( now ); 62 | arguments.rant.setModifiedDate( now ); 63 | 64 | queryExecute( 65 | "insert into rants 66 | set 67 | body = :body, 68 | userID = :userID, 69 | createdDate = :createdDate, 70 | modifiedDate = :modifiedDate 71 | ", 72 | { 73 | body: { value: "#arguments.rant.getBody()#", type: "cf_sql_longvarchar" }, 74 | userID: { value: "#arguments.rant.getuserID()#", type: "cf_sql_numeric" }, 75 | createdDate: { value: "#arguments.rant.getCreatedDate()#", type: "cf_sql_timestamp" }, 76 | modifiedDate: { value: "#arguments.rant.getModifiedDate()#", type: "cf_sql_timestamp" } 77 | }, 78 | { result: "local.result" } 79 | ); 80 | arguments.rant.setID( local.result.generatedKey ); 81 | return arguments.rant; 82 | } 83 | 84 | function update( required Rant rant ) { 85 | var now = now(); 86 | arguments.rant.setModifiedDate( now ); 87 | queryExecute( 88 | "update rants 89 | set 90 | body = :body, 91 | modifiedDate = :modifiedDate 92 | where id = :rantID 93 | ", 94 | { 95 | rantID: { value: "#arguments.rant.getID()#", type: "cf_sql_integer" }, 96 | body: { value: "#arguments.rant.getBody()#", type: "cf_sql_longvarchar" }, 97 | modifiedDate: { value: "#arguments.rant.getModifiedDate()#", type: "cf_sql_timestamp" } 98 | }, 99 | { result: "local.result" } 100 | ); 101 | return local.result; 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v5/models/UserService.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * I am the User Service v5 3 | */ 4 | component extends="v5.models.BaseService" singleton accessors="true" { 5 | 6 | /** 7 | * Constructor 8 | */ 9 | UserService function init() { 10 | super.init( 11 | entityName = "user", 12 | tableName = "users", 13 | parameterName = "userID", 14 | moduleName = "v5" 15 | ) 16 | return this; 17 | } 18 | 19 | User function get( required numeric userID ) { 20 | var q = queryExecute( 21 | "select * from users 22 | where id = :userID", 23 | { userID: { value: "#userID#", type: "cf_sql_numeric" } }, 24 | { returntype: "array" } 25 | ); 26 | if ( q.len() ) { 27 | return populator.populateFromStruct( new (), q[ 1 ] ); 28 | } else { 29 | return new () 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v6/ModuleConfig.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | Module Directives as public properties 3 | this.title = "Title of the module"; 4 | this.author = "Author of the module"; 5 | this.webURL = "Web URL for docs purposes"; 6 | this.description = "Module description"; 7 | this.version = "Module Version"; 8 | this.viewParentLookup = (true) [boolean] (Optional) // If true, checks for views in the parent first, then it the module.If false, then modules first, then parent. 9 | this.layoutParentLookup = (true) [boolean] (Optional) // If true, checks for layouts in the parent first, then it the module.If false, then modules first, then parent. 10 | this.entryPoint = "" (Optional) // If set, this is the default event (ex:forgebox:manager.index) or default route (/forgebox) the framework 11 | will use to create an entry link to the module. Similar to a default event. 12 | this.cfmapping = "The CF mapping to create"; 13 | this.modelNamespace = "The namespace to use for registered models, if blank it uses the name of the module." 14 | this.dependencies = "The array of dependencies for this module" 15 | 16 | structures to create for configuration 17 | - parentSettings : struct (will append and override parent) 18 | - settings : struct 19 | - interceptorSettings : struct of the following keys ATM 20 | - customInterceptionPoints : string list of custom interception points 21 | - interceptors : array 22 | - layoutSettings : struct (will allow to define a defaultLayout for the module) 23 | - routes : array Allowed keys are same as the addRoute() method of the SES interceptor. 24 | - wirebox : The wirebox DSL to load and use 25 | 26 | Available objects in variable scope 27 | - controller 28 | - appMapping (application mapping) 29 | - moduleMapping (include,cf path) 30 | - modulePath (absolute path) 31 | - log (A pre-configured logBox logger object for this object) 32 | - binder (The wirebox configuration binder) 33 | - wirebox (The wirebox injector) 34 | 35 | Required Methods 36 | - configure() : The method ColdBox calls to configure the module. 37 | 38 | Optional Methods 39 | - onLoad() : If found, it is fired once the module is fully loaded 40 | - onUnload() : If found, it is fired once the module is unloaded 41 | 42 | */ 43 | component { 44 | 45 | // Module Properties 46 | this.title = "v6"; 47 | this.author = ""; 48 | this.webURL = ""; 49 | this.description = ""; 50 | this.version = "1.0.0"; 51 | // If true, looks for views in the parent first, if not found, then in the module. Else vice-versa 52 | this.viewParentLookup = true; 53 | // If true, looks for layouts in the parent first, if not found, then in module. Else vice-versa 54 | this.layoutParentLookup = true; 55 | // Module Entry Point 56 | this.entryPoint = "v6"; 57 | // Inherit entry point from parent, so this will be /api/v1 58 | this.inheritEntryPoint = true; 59 | // Model Namespace 60 | this.modelNamespace = "v6"; 61 | // CF Mapping 62 | this.cfmapping = "v6"; 63 | // Auto-map models 64 | this.autoMapModels = true; 65 | // Module Dependencies 66 | this.dependencies = []; 67 | 68 | function configure() { 69 | // parent settings 70 | parentSettings = {}; 71 | 72 | // module settings - stored in modules.name.settings 73 | settings = {}; 74 | 75 | // Layout Settings 76 | layoutSettings = { defaultLayout: "" }; 77 | 78 | // SES Routes: config/Router.cfc 79 | 80 | // Custom Declared Points 81 | interceptorSettings = { customInterceptionPoints: "" }; 82 | 83 | // Custom Declared Interceptors 84 | interceptors = []; 85 | 86 | // Binder Mappings 87 | // binder.map("Alias").to("#moduleMapping#.model.MyService"); 88 | } 89 | 90 | /** 91 | * Fired when the module is registered and activated. 92 | */ 93 | function onLoad() { 94 | } 95 | 96 | /** 97 | * Fired when the module is unregistered and unloaded 98 | */ 99 | function onUnload() { 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v6/config/Router.cfc: -------------------------------------------------------------------------------- 1 | component { 2 | 3 | function configure() { 4 | resources( resource = "rants", parameterName = "rantID", except = [ "new", "edit" ] ); 5 | 6 | route( "/", "echo.index" ); 7 | route( "/:handler/:action" ).end(); 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v6/handlers/Echo.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * My RESTFul Event Handler which inherits from the module `api` 3 | */ 4 | component extends="api.handlers.BaseHandler" { 5 | 6 | // OPTIONAL HANDLER PROPERTIES 7 | this.prehandler_only = ""; 8 | this.prehandler_except = ""; 9 | this.posthandler_only = ""; 10 | this.posthandler_except = ""; 11 | this.aroundHandler_only = ""; 12 | this.aroundHandler_except = ""; 13 | 14 | // REST Allowed HTTP Methods Ex: this.allowedMethods = {delete='POST,DELETE',index='GET'} 15 | this.allowedMethods = {}; 16 | 17 | /** 18 | * Index 19 | */ 20 | any function index( event, rc, prc ) { 21 | prc.response.setData( "Welcome to my ColdBox RESTFul Service v6" ); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v6/handlers/Rants.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * My RESTFul Rants Event Handler which inherits from the module `api` 3 | */ 4 | component displayName="Rants" extends="api.handlers.BaseHandler" { 5 | 6 | // DI 7 | property name="rantService" inject="RantService@v6"; 8 | property name="userService" inject="UserService@v6"; 9 | 10 | 11 | /** 12 | * @route (GET) /api/v6/rants 13 | * 14 | * Returns a list of Rants 15 | * 16 | * @response-200 /resources/apidocs/api-v6/Rants/index/responses.json##200 17 | */ 18 | any function index( event, rc, prc ) { 19 | prc.response.setData( rantService.listArray() ); 20 | } 21 | 22 | /** 23 | * @route (GET) /api/v6/rants/:rantID 24 | * 25 | * Display a single Rant. 26 | * 27 | * @x-parameters /resources/apidocs/api-v6/Rants/show/parameters.json##parameters 28 | * @response-200 /resources/apidocs/api-v6/Rants/show/responses.json##200 29 | * @response-404 /resources/apidocs/_responses/rant.404.json 30 | */ 31 | function show( event, rc, prc ) { 32 | var validationResults = validateOrFail( 33 | target = rc, 34 | constraints = { rantID: { required: true, type: "numeric" } } 35 | ); 36 | prc.response.setData( rantService.getOrFail( rc.rantID ).getMemento() ) 37 | } 38 | 39 | /** 40 | * @route (DELETE) /api/v6/rants/:rantID 41 | * 42 | * Delete a single Rant. 43 | * 44 | * @x-parameters /resources/apidocs/api-v6/Rants/delete/parameters.json##parameters 45 | * @response-200 /resources/apidocs/api-v6/Rants/delete/responses.json##200 46 | * @response-404 /resources/apidocs/_responses/rant.404.json 47 | */ 48 | function delete( event, rc, prc ) { 49 | var validationResults = validateOrFail( 50 | target = rc, 51 | constraints = { rantID: { required: true, type: "numeric" } } 52 | ); 53 | rantService.getOrFail( rc.rantID ).delete(); 54 | prc.response.addMessage( "Rant deleted" ); 55 | } 56 | 57 | /** 58 | * @route (POST) /api/v1/rants 59 | * 60 | * Creates a new Rant. 61 | * 62 | * @requestBody /resources/apidocs/api-v6/Rants/create/requestBody.json 63 | * @response-200 /resources/apidocs/api-v6/Rants/create/responses.json##200 64 | * @response-412 /resources/apidocs/api-v6/Rants/create/responses.json##412 65 | */ 66 | function create( event, rc, prc ) { 67 | var validationResults = validateOrFail( 68 | target = rc, 69 | constraints = rantService.getConstraints() 70 | ); 71 | userService.existsOrFail( rc.userID ); 72 | var result = rantService 73 | .new( validationResults ) 74 | .validateOrFail( rantService.getConstraints() ) 75 | .save(); 76 | prc.response.setData( { "rantID": result.getID() } ); 77 | prc.response.addMessage( "Rant created" ); 78 | } 79 | 80 | /** 81 | * @route (PUT) /api/v1/rants/:rantID 82 | * 83 | * Update an existing Rant. 84 | * 85 | * @requestBody /resources/apidocs/api-v6/Rants/update/requestBody.json 86 | * @response-200 /resources/apidocs/api-v6/Rants/update/responses.json##200 87 | * @response-412 /resources/apidocs/api-v6/Rants/update/responses.json##412 88 | */ 89 | function update( event, rc, prc ) { 90 | var validationResults = validateOrFail( 91 | target = rc, 92 | constraints = rantService.addConstraints( { 93 | rantID: { required: true, type: "numeric" } 94 | } ) 95 | ); 96 | userService.existsOrFail( rc.userID ); 97 | rantService 98 | .getOrFail( rc.rantID ) 99 | .populate( validationResults ) 100 | .setID( rc.rantID ) 101 | .validateOrFail( 102 | constraints = rantService.addConstraints( { 103 | ID: { required: true, type: "numeric" } 104 | }) 105 | ) 106 | .save(); 107 | prc.response.addMessage( "Rant Updated" ); 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v6/models/BaseEntity.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * I am the Base Entity 3 | */ 4 | component accessors="true" { 5 | 6 | property name="pk"; 7 | property name="entityName"; 8 | property name="serviceName"; 9 | property name="moduleName"; 10 | 11 | // DI Injection 12 | property name="wirebox" inject="wirebox"; 13 | property name="coldbox" inject="coldbox"; 14 | 15 | /** 16 | * Initialize Entity - stores information needed 17 | * 18 | * @pk The name of the primary key field in the entity 19 | * @entityName The name of the entity so we can reference it for calls to related DAO and Service. Set as optional for backwards 20 | */ 21 | function init( 22 | pk = "id", 23 | entityName = "", 24 | serviceName = "", 25 | moduleName = "" 26 | ) { 27 | setPk( arguments.pk ); 28 | if ( len( entityName ) ) { 29 | setEntityName( arguments.entityName ); 30 | } 31 | if ( arguments.serviceName != "" ) { 32 | setServiceName( arguments.serviceName ); 33 | } else { 34 | setServiceName( arguments.entityName & "Service" ); 35 | } 36 | setModuleName( arguments.moduleName ); 37 | if ( arguments.moduleName != "" ) { 38 | setServiceName( getServiceName() & "@" & arguments.moduleName ); 39 | } 40 | } 41 | 42 | /** 43 | * Verify if entity is loaded or not 44 | */ 45 | boolean function isLoaded() { 46 | return ( isNull( variables[ getPk() ] ) OR !len( variables[ getPk() ] ) ? false : true ); 47 | } 48 | 49 | /** 50 | * Populate a model object from the request Collection or a passed in memento structure 51 | * @scope Use scope injection instead of setters population. Ex: scope=variables.instance. 52 | * @trustedSetter If set to true, the setter method will be called even if it does not exist in the object 53 | * @include A list of keys to include in the population 54 | * @exclude A list of keys to exclude in the population 55 | * @ignoreEmpty Ignore empty values on populations, great for ORM population 56 | * @nullEmptyInclude A list of keys to NULL when empty 57 | * @nullEmptyExclude A list of keys to NOT NULL when empty 58 | * @composeRelationships Automatically attempt to compose relationships from memento 59 | * @memento A structure to populate the model, if not passed it defaults to the request collection 60 | * @jsonstring If you pass a json string, we will populate your model with it 61 | * @xml If you pass an xml string, we will populate your model with it 62 | * @qry If you pass a query, we will populate your model with it 63 | * @rowNumber The row of the qry parameter to populate your model with 64 | */ 65 | function populate( 66 | struct memento = coldbox 67 | .getRequestService() 68 | .getContext() 69 | .getCollection(), 70 | scope = "", 71 | boolean trustedSetter = false, 72 | include = "", 73 | exclude = "", 74 | boolean ignoreEmpty = false, 75 | nullEmptyInclude = "", 76 | nullEmptyExclude = "", 77 | boolean composeRelationships = false, 78 | string jsonstring, 79 | string xml, 80 | query qry 81 | ) { 82 | arguments[ "model" ] = this; 83 | arguments.target = this; 84 | 85 | // json? 86 | if ( structKeyExists( arguments, "jsonstring" ) ) { 87 | return wirebox.getObjectPopulator().populateFromJSON( argumentCollection = arguments ); 88 | } 89 | // XML 90 | else if ( structKeyExists( arguments, "xml" ) ) { 91 | return wirebox.getObjectPopulator().populateFromXML( argumentCollection = arguments ); 92 | } 93 | // Query 94 | else if ( structKeyExists( arguments, "qry" ) ) { 95 | return wirebox.getObjectPopulator().populateFromQuery( argumentCollection = arguments ); 96 | } 97 | // Mementos 98 | else { 99 | // populate 100 | return wirebox.getObjectPopulator().populateFromStruct( argumentCollection = arguments ); 101 | } 102 | } 103 | 104 | /** 105 | * Validate an object or structure according to the constraints rules. 106 | * @fields The fields to validate on the target. By default, it validates on all fields 107 | * @constraints A structure of constraint rules or the name of the shared constraint rules to use for validation 108 | * @locale The i18n locale to use for validation messages 109 | * @excludeFields The fields to exclude from the validation 110 | * @includeFields The fields to include in the validation 111 | * 112 | * @return cbvalidation.model.result.IValidationResult 113 | * @throws ValidationException error 114 | */ 115 | public struct function validateOrFail( 116 | any constraints = {}, 117 | string fields = "*", 118 | string locale = "", 119 | string excludeFields = "", 120 | string includeFields = "" 121 | ) { 122 | var result = wirebox 123 | .getInstance( "ValidationManager@cbvalidation" ) 124 | .validate( 125 | target = this, 126 | fields = arguments.fields, 127 | constraints = arguments.constraints, 128 | locale = arguments.locale, 129 | excludeFields = arguments.excludeFields, 130 | includeFields = arguments.includeFields 131 | ); 132 | if ( result.hasErrors() ) { 133 | throw( 134 | type = "ValidationException", 135 | message = "The Model #getEntityName()# failed to pass validation", 136 | extendedInfo = serializeJSON( result.getAllErrors() ) 137 | ); 138 | } 139 | return this; 140 | } 141 | 142 | function save() { 143 | if ( isLoaded() ) { 144 | return wirebox.getInstance( "#getServiceName()#" ).update( this ); 145 | } else { 146 | return wirebox.getInstance( "#getServiceName()#" ).create( this ); 147 | } 148 | } 149 | 150 | function delete() { 151 | return wirebox.getInstance( "#getServiceName()#" ).delete( this.getID() ); 152 | } 153 | 154 | this.memento = { 155 | // An array of the properties/relationships to include by default 156 | defaultIncludes: [ "*" ], 157 | // An array of properties/relationships to exclude by default 158 | defaultExcludes: [], 159 | // An array of properties/relationships to NEVER include 160 | neverInclude: [ 161 | "entityName", 162 | "pk", 163 | "serviceName", 164 | "moduleName" 165 | ], // A struct of defaults for properties/relationships if they are null 166 | defaults: {}, 167 | // A struct of mapping functions for properties/relationships that can transform them 168 | mappers: {} 169 | } 170 | 171 | } 172 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v6/models/BaseService.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * I am the Base Service 3 | */ 4 | component accessors="true" { 5 | 6 | property name="entityName"; 7 | property name="tableName"; 8 | property name="primaryKey"; 9 | // property name="parameterName"; 10 | property name="serviceName"; 11 | property name="moduleName"; 12 | 13 | function init( 14 | entityName, 15 | tableName, 16 | primaryKey = "id", 17 | // parameterName="", 18 | serviceName = "", 19 | moduleName = "" 20 | ) { 21 | setEntityName( arguments.entityName ); 22 | setTableName( arguments.tableName ); 23 | setPrimaryKey( arguments.primaryKey ); 24 | // if( arguments.parameterName != "" ){ 25 | // setParameterName( arguments.parameterName ); 26 | // } else { 27 | // setParameterName( arguments.primaryKey ); 28 | // } 29 | if ( arguments.serviceName != "" ) { 30 | setServiceName( arguments.serviceName ); 31 | } else { 32 | setServiceName( arguments.entityName & "Service" ); 33 | } 34 | setModuleName( arguments.moduleName ); 35 | if ( arguments.moduleName != "" ) { 36 | setServiceName( getServiceName() & "@" & arguments.moduleName ); 37 | } 38 | } 39 | 40 | /** 41 | * Return a new Entity, empty or pre-populated with passed in data 42 | * 43 | * @data Data to populate the new Entity with 44 | */ 45 | function new( struct data = {} ) { 46 | // if( data.isEmpty() ){ 47 | 48 | // } else { 49 | var modelName = getEntityName(); 50 | if ( getModuleName() != "" ) { 51 | modelName = modelName & "@" & getModuleName(); 52 | } 53 | return populator.populateFromStruct( target = wireBox.getInstance( "#modelName#" ), memento = arguments.data ) 54 | // } 55 | } 56 | 57 | /** 58 | * Check to see if there is a row with a matching primary key in the database. Much faster than a full entity query and object load 59 | * 60 | * @return Returns true if there is a row with the matching Primary Key, otherwise returns false 61 | */ 62 | boolean function exists() { 63 | return booleanFormat( 64 | queryExecute( 65 | "select id from #getTableName()# 66 | where #getPrimaryKey()# = :id", 67 | { id: { value: arguments[ 1 ], type: "cf_sql_numeric" } }, 68 | { returntype: "array" } 69 | ).len() 70 | ) 71 | } 72 | 73 | /** 74 | * Check to see if there is a row with a matching primary key in the database. Much faster than a full entity query and object load 75 | * 76 | * @return Returns true if there is a row with the matching Primary Key 77 | * @throws EntityNotFound if the entity is not found 78 | */ 79 | function existsOrFail() { 80 | if ( exists( argumentCollection = arguments ) ) { 81 | return true; 82 | } else { 83 | throw( type = "EntityNotFound", message = "#entityName# Not Found" ); 84 | } 85 | } 86 | 87 | /** 88 | * Query and load an entity if possible, else throw an error 89 | * 90 | * @return Returns the Entity if there is a row with the matching Primary Key 91 | * @throws EntityNotFound if the entity is not found 92 | */ 93 | function getOrFail() { 94 | var maybeEntity = this.get( argumentCollection = arguments ); 95 | if ( isNull( maybeEntity ) || !maybeEntity.isLoaded() ) { 96 | throw( type = "EntityNotFound", message = "#getEntityName()# Not Found" ); 97 | } 98 | return maybeEntity; 99 | } 100 | 101 | /** 102 | * Helper to get Entity Constraints 103 | * 104 | * @constraintsKeyName The name of the variable with the entity constraints inside of the entity. Defaults to the convention of `constraints` 105 | * 106 | * @return a CBValidation compliant struct of Entity Constraints 107 | */ 108 | function getConstraints( string constraintsKeyName = "constraints" ) { 109 | return new ( {} )[ arguments.constraintsKeyName ]; 110 | } 111 | 112 | 113 | /** 114 | * Helper to add a structure of Constraints into the existing entity constraints and return the combined struct 115 | * 116 | * @newConstraints The new struct full of constraints to add 117 | * @constraintsKeyName The name of the variable with the entity constraints inside of the entity. Defaults to the convention of `constraints` 118 | * 119 | * @return a CBValidation compliant struct of Entity Constraints 120 | */ 121 | function addConstraints( newConstraints = {}, constraintsKeyName = "constraints" ) { 122 | var constraints = getConstraints( arguments.constraintsKeyName ); 123 | constraints.append( arguments.newConstraints ); 124 | return constraints; 125 | } 126 | 127 | } 128 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v6/models/Rant.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * I am a new Rant Object 3 | */ 4 | component extends="v6.models.BaseEntity" accessors="true" { 5 | 6 | // DI 7 | property name="userService" inject="UserService@v6"; 8 | 9 | // Properties 10 | property name="id" type="string"; 11 | property name="body" type="string"; 12 | property name="createdDate" type="date"; 13 | property name="modifiedDate" type="date"; 14 | property name="userID" type="string"; 15 | 16 | 17 | /** 18 | * Constructor 19 | */ 20 | Rant function init() { 21 | super.init( entityName = "rant", moduleName = "v6" ); 22 | return this; 23 | } 24 | 25 | this.constraints = { 26 | userID: { required: true, type: "numeric" }, 27 | body: { required: true } 28 | }; 29 | 30 | /** 31 | * getUser 32 | */ 33 | function getUser() { 34 | return userService.get( getUserID() ); 35 | } 36 | 37 | // function getMemento() { 38 | // return { 39 | // "id" = getID(), 40 | // "body" = getBody(), 41 | // "createdDate" = dateFormat( getCreatedDate(), "long" ), 42 | // "modifiedDate" = dateFormat( getModifiedDate(), "long" ), 43 | // "userId" = getUserID() 44 | // }; 45 | // } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v6/models/RantService.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * I am the Rant Service v6 3 | */ 4 | component extends="v6.models.BaseService" singleton accessors="true" { 5 | 6 | // DI Injection 7 | property name="wirebox" inject="wirebox"; 8 | property name="populator" inject="wirebox:populator"; 9 | 10 | /** 11 | * Constructor 12 | */ 13 | RantService function init() { 14 | super.init( 15 | entityName = "rant", 16 | tableName = "rants", 17 | parameterName = "rantID", 18 | moduleName = "v6" 19 | ) 20 | return this; 21 | } 22 | 23 | function list() { 24 | return this 25 | .listArray() 26 | .map( function( rant ) { 27 | return populator.populateFromStruct( new (), rant ); 28 | } ); 29 | } 30 | 31 | function listArray() { 32 | return queryExecute( "select * from rants ORDER BY createdDate DESC", {}, { returntype: "array" } ) 33 | } 34 | 35 | Rant function get( required numeric rantID ) { 36 | var q = queryExecute( 37 | "select * from rants 38 | where id = :rantID", 39 | { rantID: { value: "#rantID#", type: "cf_sql_numeric" } }, 40 | { returntype: "array" } 41 | ); 42 | if ( q.len() ) { 43 | return populator.populateFromStruct( new (), q[ 1 ] ); 44 | } else { 45 | return new () 46 | } 47 | } 48 | 49 | function delete( required numeric rantID ) { 50 | queryExecute( 51 | "delete from rants 52 | where id = :rantID", 53 | { rantID: { value: "#rantID#", type: "cf_sql_numeric" } }, 54 | { result: "local.result" } 55 | ); 56 | return local.result; 57 | } 58 | 59 | function create( required Rant rant ) { 60 | var now = now(); 61 | arguments.rant.setCreatedDate( now ); 62 | arguments.rant.setModifiedDate( now ); 63 | 64 | queryExecute( 65 | "insert into rants 66 | set 67 | body = :body, 68 | userID = :userID, 69 | createdDate = :createdDate, 70 | modifiedDate = :modifiedDate 71 | ", 72 | { 73 | body: { value: "#arguments.rant.getBody()#", type: "cf_sql_longvarchar" }, 74 | userID: { value: "#arguments.rant.getuserID()#", type: "cf_sql_numeric" }, 75 | createdDate: { value: "#arguments.rant.getCreatedDate()#", type: "cf_sql_timestamp" }, 76 | modifiedDate: { value: "#arguments.rant.getModifiedDate()#", type: "cf_sql_timestamp" } 77 | }, 78 | { result: "local.result" } 79 | ); 80 | arguments.rant.setID( local.result.generatedKey ); 81 | return arguments.rant; 82 | } 83 | 84 | function update( required Rant rant ) { 85 | var now = now(); 86 | arguments.rant.setModifiedDate( now ); 87 | queryExecute( 88 | "update rants 89 | set 90 | body = :body, 91 | modifiedDate = :modifiedDate 92 | where id = :rantID 93 | ", 94 | { 95 | rantID: { value: "#arguments.rant.getID()#", type: "cf_sql_integer" }, 96 | body: { value: "#arguments.rant.getBody()#", type: "cf_sql_longvarchar" }, 97 | modifiedDate: { value: "#arguments.rant.getModifiedDate()#", type: "cf_sql_timestamp" } 98 | }, 99 | { result: "local.result" } 100 | ); 101 | return local.result; 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v6/models/UserService.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * I am the User Service v6 3 | */ 4 | component extends="v6.models.BaseService" singleton accessors="true" { 5 | 6 | /** 7 | * Constructor 8 | */ 9 | UserService function init() { 10 | super.init( 11 | entityName = "user", 12 | tableName = "users", 13 | parameterName = "userID", 14 | moduleName = "v6" 15 | ) 16 | return this; 17 | } 18 | 19 | User function get( required numeric userID ) { 20 | var q = queryExecute( 21 | "select * from users 22 | where id = :userID", 23 | { userID: { value: "#userID#", type: "cf_sql_numeric" } }, 24 | { returntype: "array" } 25 | ); 26 | if ( q.len() ) { 27 | return populator.populateFromStruct( new (), q[ 1 ] ); 28 | } else { 29 | return new () 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # 2020-ITB - Modern Functional & Fluent CFML REST APIs 2 | 3 | This repo is the demo repo for the presentation given at Into the Box 2020. 4 | 5 | ## Slides: 6 | 7 | The slides can be found at: https://drive.google.com/file/d/1C-__UfDdvvjpIGKtx4B_J14Z2frnJUKL/view?usp=sharing 8 | 9 | ## App Setup 10 | 11 | ### Modules needed 12 | 13 | This app works with CommandBox, it also uses a couple of CommandBox system modules which are useful in most CommandBox projects ( in box.json for simplicity ): 14 | 15 | - CFConfig CLI - https://www.forgebox.io/view/commandbox-cfconfig 16 | - CommandBox dotenv - https://www.forgebox.io/view/commandbox-dotenv 17 | - CFFormat - https://www.forgebox.io/view/commandbox-cfformat 18 | - 19 | Recommened but not required: 20 | 21 | - commandbox-cflint - https://www.forgebox.io/view/commandbox-cflint 22 | 23 | ### Database setup 24 | 25 | The database needed for this is MySQL 5.7. The SQL file for this project is located in the /workbench/database folder. Please use that to seed your database, and call the database fluentAPI for consistency with the .env.example file provided. 26 | 27 | ### Config setup 28 | 29 | Please copy the .env.example file into a new file you can create called .env 30 | You could use the `dotenv populate` command for a wizard to help you make that file. 31 | 32 | Change the host, username and password of the database server you intend to use. 33 | 34 | ### Install Dependencies 35 | 36 | The box.json stores all of the dependencies of the app, these are not stored in the repo, so please use the command below to install these dependencies ( using ForgeBox behind the scenes ) 37 | 38 | `box install` 39 | 40 | ### Start the app 41 | 42 | Once you have your .env, your db loaded, and your box.json dependencies installed, you can start your server. 43 | 44 | `box start` 45 | 46 | ## What can you do in the app? 47 | 48 | Apart from hitting the root of the site, which is an API echo response, there are lots of things you can do with this app 49 | 50 | http://127.0.0.1:60146/ 51 | 52 | ### Hit API Endpoints 53 | 54 | - List rants: http://127.0.0.1:60146/api/v6/rants 55 | - Create Rants 56 | - Read Rants 57 | - Update Rants 58 | - Delete Rants 59 | 60 | ### View the Tests 61 | 62 | http://127.0.0.1:60146/tests/runner.cfm 63 | 64 | ### View API Docs 65 | 66 | http://127.0.0.1:60146/cbswagger 67 | 68 | #### What can you do with the API Docs? 69 | 70 | - Import into Postman: https://www.postman.com/ 71 | - Use with Swagger.io site: https://editor.swagger.io/ 72 | 73 | ### View Route Visualizer 74 | 75 | http://127.0.0.1:60146/route-visualizer -------------------------------------------------------------------------------- /rest-readme.md: -------------------------------------------------------------------------------- 1 | # REST HMVC Template 2 | 3 | This template gives you the base for building RESTFul web services with ColdBox in a modular fashion. This template will create an `api` module with a `v1` sub-module within it. It will leverage ColdBox 5 modular inherit entry points to mimic the URL resources to your modular design. 4 | 5 | In the `api/models` folder you will find our Universal REST Response object that can be leveraged as your base for building RESTFul services. 6 | 7 | ``` 8 | + modules_app 9 | + api 10 | + models 11 | + modules_app 12 | + v1 13 | ``` 14 | 15 | ## Implicit Methods 16 | The base handler implements an around handler approach to provide consistency and the following actions: 17 | 18 | - `onError` - Fires whenever there is a runtime exception in any action 19 | - `onInvalidHTTPMethod` - Fires on invalid HTTP method access 20 | - `onMissingAction` - Fires on invalid missing actions on handlers 21 | 22 | ## Utility Functions 23 | We also give you some utility functions for RESTFul building: 24 | 25 | - `routeNotFound` - Can be used to fire of route not founds via 404 26 | - `onExpectationFailed` - Can be called when an expectation of a request fails, like invalid parameters/headers etc. 27 | - `onAuthorizationFailure` - Can be called to send a NOT Authorized status code and message. 28 | 29 | ## HTTP Security 30 | By default the base handlers leverages ColdBox method security via the `this.allowedMethods` structure: 31 | 32 | ``` 33 | this.allowedMethods = { 34 | "index" : METHODS.GET, 35 | "get" : METHODS.GET, 36 | "list" : METHODS.GET, 37 | "update" : METHODS.PUT & "," & METHODS.PATCH, 38 | "delete" : METHODS.DELETE 39 | }; 40 | ``` 41 | 42 | ## HTTP Methods 43 | The base handler contains a static construct called `METHODS` that implements basic HTTP Methods that you can use for messages and allowed methods. 44 | 45 | ``` 46 | METHODS = { 47 | "HEAD" : "HEAD", 48 | "GET" : "GET", 49 | "POST" : "POST", 50 | "PATCH" : "PATCH", 51 | "PUT" : "PUT", 52 | "DELETE" : "DELETE" 53 | }; 54 | ``` 55 | 56 | ## Status Codes 57 | The base handler contains a static construct called `STATUS` that implements basic HTTP status codes you can use: 58 | 59 | ``` 60 | STATUS = { 61 | "CREATED" : 201, 62 | "ACCEPTED" : 202, 63 | "SUCCESS" : 200, 64 | "NO_CONTENT" : 204, 65 | "RESET" : 205, 66 | "PARTIAL_CONTENT" : 206, 67 | "BAD_REQUEST" : 400, 68 | "NOT_AUTHORIZED" : 401, 69 | "NOT_FOUND" : 404, 70 | "NOT_ALLOWED" : 405, 71 | "NOT_ACCEPTABLE" : 406, 72 | "TOO_MANY_REQUESTS" : 429, 73 | "EXPECTATION_FAILED" : 417, 74 | "INTERNAL_ERROR" : 500, 75 | "NOT_IMPLEMENTED" : 501 76 | }; 77 | ``` 78 | 79 | 80 | - 81 | 82 | ## License 83 | Apache License, Version 2.0. 84 | 85 | ## Important Links 86 | 87 | Source Code 88 | - https://github.com/coldbox-templates/rest-hmvc 89 | 90 | ## Quick Installation 91 | 92 | Each application templates contains a `box.json` so it can leverage [CommandBox](http://www.ortussolutions.com/products/commandbox) for its dependencies. 93 | Just go into each template directory and type: 94 | 95 | ``` 96 | box install 97 | ``` 98 | 99 | This will setup all the needed dependencies for each application template. You can then type: 100 | 101 | ``` 102 | box server start 103 | ``` 104 | 105 | And run the application. 106 | 107 | --- 108 | 109 | ### THE DAILY BREAD 110 | > "I am the way, and the truth, and the life; no one comes to the Father, but by me (JESUS)" Jn 14:1-12 -------------------------------------------------------------------------------- /robots.txt: -------------------------------------------------------------------------------- 1 |  2 | User-agent: Slurp 3 | Crawl-delay: 100 4 | Disallow: 5 | 6 | User-agent: gsa-crawler-www 7 | Crawl-delay: 100 8 | 9 | User-agent: Googlebot 10 | Crawl-delay: 100 11 | 12 | User-agent: Mediapartners-Google 13 | Disallow: 14 | 15 | User-agent: Yahoo-NewsCrawler 16 | Disallow: 17 | 18 | User-Agent: msnbot 19 | Crawl-delay: 100 20 | Disallow: 21 | 22 | User-Agent: * 23 | Disallow: /config/ 24 | Disallow: /handlers/ 25 | Disallow: /includes/ 26 | Disallow: /interceptors/ 27 | Disallow: /layouts/ 28 | Disallow: /logs/ 29 | Disallow: /models/ 30 | Disallow: /modules/ 31 | Disallow: /modules_app/ 32 | Disallow: /views/ 33 | Allow: / -------------------------------------------------------------------------------- /server.json: -------------------------------------------------------------------------------- 1 | { 2 | "web":{ 3 | "rewrites":{ 4 | "enable":true 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /tests/Application.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | ******************************************************************************** 3 | Copyright 2005-2007 ColdBox Framework by Luis Majano and Ortus Solutions, Corp 4 | www.ortussolutions.com 5 | ******************************************************************************** 6 | */ 7 | component{ 8 | 9 | // APPLICATION CFC PROPERTIES 10 | this.name = "ColdBoxTestingSuite" & hash(getCurrentTemplatePath()); 11 | this.sessionManagement = true; 12 | this.sessionTimeout = createTimeSpan( 0, 0, 15, 0 ); 13 | this.applicationTimeout = createTimeSpan( 0, 0, 15, 0 ); 14 | this.setClientCookies = true; 15 | this.datasource = "fluentAPI"; 16 | 17 | // Create testing mapping 18 | this.mappings[ "/tests" ] = getDirectoryFromPath( getCurrentTemplatePath() ); 19 | // Map back to its root 20 | rootPath = REReplaceNoCase( this.mappings[ "/tests" ], "tests(\\|/)", "" ); 21 | this.mappings["/root"] = rootPath; 22 | 23 | public void function onRequestEnd() { 24 | structDelete( application, "cbController" ); 25 | structDelete( application, "wirebox" ); 26 | } 27 | } -------------------------------------------------------------------------------- /tests/index.cfm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | #testbox.init( directory=rootMapping & url.path ).run()# 27 | 28 |

Invalid incoming directory: #rootMapping & url.path#

29 |
30 | 31 | 32 |
33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | TestBox Browser 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 |
66 |
67 |
68 | 69 |
70 | v#testbox.getVersion()# 71 |
72 | 73 |
74 |
75 |
76 |
77 |
78 | 79 |

TestBox Test Browser:

80 |

81 | Below is a listing of the files and folders starting from your root #rootPath#. You can click on individual tests in order to execute them 82 | or click on the Run All button on your left and it will execute a directory runner from the visible folder. 83 |

84 | 85 |
86 | Contents: #executePath# 87 | 88 |

89 |
90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | ✚ #qResults.name#
98 | 99 | target="_blank"
>#qResults.name#
100 | 101 | target="_blank">#qResults.name#
102 | 103 | #qResults.name#
104 | 105 | 106 |
107 |
108 |
109 |
110 |
111 |
112 | 113 | 114 | 115 |
-------------------------------------------------------------------------------- /tests/resources/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gpickin/itb2020-fluentAPI/f005189807e2671da34f4be8383043c7d174171e/tests/resources/.gitkeep -------------------------------------------------------------------------------- /tests/resources/BaseTest.cfc: -------------------------------------------------------------------------------- 1 | component extends="coldbox.system.testing.BaseTestCase" appMapping="/root" { 2 | 3 | // Do not unload per test bundle to improve performance. 4 | this.unloadColdBox = false; 5 | 6 | /*********************************** LIFE CYCLE Methods ***********************************/ 7 | 8 | // executes before all suites+specs in the run() method 9 | function beforeAll(){ 10 | super.beforeAll(); 11 | 12 | var lengthTest = function( expectation, args = {} ) { 13 | // handle both positional and named arguments 14 | param args.value = ""; 15 | if ( structKeyExists( args, 1 ) ) { 16 | args.value = args[ 1 ]; 17 | } 18 | 19 | param args.message = ""; 20 | if ( structKeyExists( args, 2 ) ) { 21 | args.message = args[ 2 ]; 22 | } 23 | 24 | param args.operator = "GT"; 25 | if ( structKeyExists( args, 3 ) ) { 26 | args.value = args[ 3 ]; 27 | } 28 | 29 | if ( !isNumeric( args.value )) { 30 | expectation.message = "The value you are testing must be a valid number"; 31 | return false; 32 | } 33 | try{ 34 | var length = expectation.actual.len(); 35 | } catch ( any e ){ 36 | expectation.message = "The length of the Item could not be found"; 37 | return false; 38 | } 39 | 40 | if( args.operator == "GT" && length <= args.value ){ 41 | expectation.message = "The length of the item was #length# - that is not GT #args.value#"; 42 | debug( expectation.actual ); 43 | return false; 44 | } else if( args.operator == "GTE" && length < args.value ){ 45 | expectation.message = "The length of the item was #length# - that is not GTE #args.value#"; 46 | debug( expectation.actual ); 47 | return false; 48 | } else if( args.operator == "LT" && length >= args.value ){ 49 | expectation.message = "The length of the item was #length# - that is not LT #args.value#"; 50 | debug( expectation.actual ); 51 | return false; 52 | } else if( args.operator == "LTE" && length > args.value ){ 53 | expectation.message = "The length of the item was #length# - that is not LTE #args.value#"; 54 | debug( expectation.actual ); 55 | return false; 56 | } 57 | 58 | return true; 59 | }; 60 | 61 | addMatchers( { 62 | toHaveStatusCode: function( expectation, args = {} ) { 63 | // handle both positional and named arguments 64 | param args.statusCode = ""; 65 | if ( structKeyExists( args, 1 ) ) { 66 | args.statusCode = args[ 1 ]; 67 | } 68 | param args.message = ""; 69 | if ( structKeyExists( args, 2 ) ) { 70 | args.message = args[ 2 ]; 71 | } 72 | 73 | if ( args.statusCode == "" ) { 74 | expectation.message = "No status code provided."; 75 | return false; 76 | } 77 | 78 | try { 79 | var statusCode = expectation.actual.getStatusCode(); 80 | } 81 | catch ( any e ) { 82 | expectation.message = "[#expecation.actual#] does not have a getStatusCode method."; 83 | debug( expectation.actual ); 84 | return false; 85 | } 86 | 87 | if ( statusCode != args.statusCode ) { 88 | expectation.message = "#args.message#. Received incorrect status code. Expected [#args.statusCode#]. Received [#statusCode#]."; 89 | debug( expectation.actual ); 90 | return false; 91 | } 92 | 93 | return true; 94 | }, 95 | toHaveKeyWithCase: function( expectation, args = {} ) { 96 | // handle both positional and named arguments 97 | param args.key = ""; 98 | if ( structKeyExists( args, 1 ) ) { 99 | args.key = args[ 1 ]; 100 | } 101 | param args.message = ""; 102 | if ( structKeyExists( args, 2 ) ) { 103 | args.message = args[ 2 ]; 104 | } 105 | 106 | if ( args.key == "" ) { 107 | expectation.message = "No Key Provided."; 108 | return false; 109 | } 110 | 111 | if( !listFind( expectation.actual.keyList(), args.key ) ){ 112 | if( listFindNoCase( expectation.actual.keyList(), args.key ) ){ 113 | expectation.message = "The key(s) [#args.key#] does exist in the target object, but the Case is incorrect. Found keys are [#structKeyArray( expectation.actual ).toString()#]"; 114 | } else { 115 | expectation.message = "The key(s) [#args.key#] does not exist in the target object, with or without case sensitivity. Found keys are [#structKeyArray( expectation.actual ).toString()#]"; 116 | } 117 | debug( expectation.actual ); 118 | return false; 119 | } 120 | 121 | return true; 122 | }, 123 | toHaveLengthGT: function( expectation, args = {}, lengthTest=lengthTest ) { 124 | args[ "operator" ] = "GT"; 125 | return lengthTest( expectation, args ); 126 | }, 127 | toHaveLengthGTE: function( expectation, args = {}, lengthTest=lengthTest ) { 128 | args[ "operator" ] = "GTE"; 129 | return lengthTest( expectation, args ); 130 | }, 131 | toHaveLengthLT: function( expectation, args = {}, lengthTest=lengthTest ) { 132 | args[ "operator" ] = "LT"; 133 | return lengthTest( expectation, args ); 134 | }, 135 | toHaveLengthLTE: function( expectation, args = {}, lengthTest=lengthTest ) { 136 | args[ "operator" ] = "LTE"; 137 | return lengthTest( expectation, args ); 138 | }, 139 | } ); 140 | getWireBox().autowire( this ); 141 | } 142 | 143 | // executes after all suites+specs in the run() method 144 | function afterAll(){ 145 | super.afterAll(); 146 | } 147 | 148 | function reset(){ 149 | structDelete( application, "wirebox" ); 150 | structDelete( application, "cbController" ); 151 | } 152 | 153 | function withRollback( spec, suite ) aroundEach { 154 | transaction{ 155 | try{ 156 | return arguments.spec.body(); 157 | } catch( any e ){ 158 | rethrow; 159 | } finally{ 160 | transaction action="rollback"; 161 | } 162 | } 163 | } 164 | 165 | } 166 | -------------------------------------------------------------------------------- /tests/runner.cfm: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /tests/specs/integration/api-v1/EchoTests.cfc: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Integration Test as BDD (CF10+ or Railo 4.1 Plus) 3 | * 4 | * Extends the integration class: coldbox.system.testing.BaseTestCase 5 | * 6 | * so you can test your ColdBox application headlessly. The 'appMapping' points by default to 7 | * the '/root' mapping created in the test folder Application.cfc. Please note that this 8 | * Application.cfc must mimic the real one in your root, including ORM settings if needed. 9 | * 10 | * The 'execute()' method is used to execute a ColdBox event, with the following arguments 11 | * * event : the name of the event 12 | * * private : if the event is private or not 13 | * * prePostExempt : if the event needs to be exempt of pre post interceptors 14 | * * eventArguments : The struct of args to pass to the event 15 | * * renderResults : Render back the results of the event 16 | *******************************************************************************/ 17 | component 18 | extends="coldbox.system.testing.BaseTestCase" 19 | appMapping="/root" 20 | { 21 | 22 | /*********************************** LIFE CYCLE Methods ***********************************/ 23 | 24 | function beforeAll() { 25 | super.beforeAll(); 26 | // do your own stuff here 27 | } 28 | 29 | function afterAll() { 30 | // do your own stuff here 31 | super.afterAll(); 32 | } 33 | 34 | /*********************************** BDD SUITES ***********************************/ 35 | 36 | function run() { 37 | describe( "My RESTFUl Service", function() { 38 | beforeEach( function( currentSpec ) { 39 | // Setup as a new ColdBox request, VERY IMPORTANT. ELSE EVERYTHING LOOKS LIKE THE SAME REQUEST. 40 | setup(); 41 | } ); 42 | 43 | it( "can handle invalid HTTP Calls", function() { 44 | var event = execute( 45 | event = "v1:echo.onInvalidHTTPMethod", 46 | renderResults = true, 47 | eventArguments = { faultAction: "test" } 48 | ); 49 | var response = event.getPrivateValue( "response" ); 50 | expect( response.getError() ).toBeTrue(); 51 | expect( response.getStatusCode() ).toBe( 405 ); 52 | } ); 53 | 54 | it( "can handle global exceptions", function() { 55 | var event = execute( 56 | event = "v1:echo.onError", 57 | renderResults = true, 58 | eventArguments = { exception: { message: "unit test", detail: "unit test", stacktrace: "" } } 59 | ); 60 | 61 | var response = event.getPrivateValue( "response" ); 62 | expect( response.getError() ).toBeTrue(); 63 | expect( response.getStatusCode() ).toBe( 500 ); 64 | } ); 65 | 66 | it( "can handle an echo", function() { 67 | var event = this.request( "/api/v1/echo/index" ); 68 | var response = event.getPrivateValue( "response" ); 69 | expect( response.getError() ).toBeFalse(); 70 | expect( response.getData() ).toBe( "Welcome to my ColdBox RESTFul Service V1" ); 71 | } ); 72 | 73 | it( "can handle missing actions", function() { 74 | var event = this.request( "/api/v1/echo/bogus" ); 75 | var response = event.getPrivateValue( "response" ); 76 | expect( response.getError() ).tobeTrue(); 77 | expect( response.getStatusCode() ).toBe( 405 ); 78 | } ); 79 | } ); 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /tests/specs/integration/api-v5/RantsTest.cfc: -------------------------------------------------------------------------------- 1 | component extends="tests.resources.BaseTest" appMapping="/" { 2 | 3 | function run() { 4 | describe( "Rants V5 API Handler", function() { 5 | beforeEach( function( currentSpec ) { 6 | // Setup as a new ColdBox request for this suite, VERY IMPORTANT. ELSE EVERYTHING LOOKS LIKE THE SAME REQUEST. 7 | setup(); 8 | } ); 9 | 10 | scenario( "Get a list of Rants", function() { 11 | given( "I make a get call to /api/v5/rants", function() { 12 | when( "I have no search filters", function() { 13 | then( "I will get a list of Rants", function() { 14 | var event = get( "/api/v5/rants" ); 15 | var returnedJSON = event.getRenderData().data; 16 | expect( returnedJSON ).toHaveKeyWithCase( "error" ); 17 | expect( returnedJSON ).toHaveKeyWithCase( "error" ); 18 | expect( returnedJSON.error ).toBeFalse(); 19 | expect( returnedJSON ).toHaveKeyWithCase( "data" ); 20 | expect( returnedJSON.data ).toBeArray(); 21 | expect( returnedJSON.data ).toHaveLengthGTE( 1 ); 22 | } ); 23 | } ); 24 | } ); 25 | } ); 26 | 27 | scenario( "Get an individual Rant", function() { 28 | given( "I make a get call to /api/v5/rants/:rantID", function() { 29 | when( "I pass an invalid rantID", function() { 30 | then( "I will get a 412 error", function() { 31 | var rantID = "x" 32 | var event = get( "/api/v5/rants/#rantID#" ); 33 | var returnedJSON = event.getRenderData().data; 34 | expect( returnedJSON ).toHaveKeyWithCase( "error" ); 35 | expect( returnedJSON.error ).toBeTrue(); 36 | expect( event ).toHaveStatusCode( 412 ); 37 | expect( returnedJSON ).toHaveKeyWithCase( "messages" ); 38 | expect( returnedJSON.messages ).toBeArray(); 39 | expect( returnedJSON.messages ).toHaveLengthGTE( 1 ); 40 | expect( returnedJSON.messages[ 1 ] ).toBe( 41 | "The 'RANTID' has an invalid type, expected type is numeric" 42 | ); 43 | } ); 44 | } ); 45 | 46 | when( "I pass a valid but non existing rantID", function() { 47 | then( "I will get a 404 error", function() { 48 | var rantID = "1" 49 | var event = get( "/api/v5/rants/#rantID#" ); 50 | var returnedJSON = event.getRenderData().data; 51 | expect( returnedJSON ).toHaveKeyWithCase( "error" ); 52 | expect( returnedJSON.error ).toBeTrue(); 53 | expect( event ).toHaveStatusCode( 404 ); 54 | expect( returnedJSON ).toHaveKeyWithCase( "messages" ); 55 | expect( returnedJSON.messages ).toBeArray(); 56 | expect( returnedJSON.messages ).toHaveLengthGTE( 1 ); 57 | expect( returnedJSON.messages[ 1 ] ).toMatch( "Rant not found" ); 58 | } ); 59 | } ); 60 | 61 | when( "I pass a valid and existing rantID", function() { 62 | then( "I will get a single Rant returned", function() { 63 | var rantID = 7; 64 | var event = get( "/api/v5/rants/#rantID#" ); 65 | var returnedJSON = event.getRenderData().data; 66 | expect( returnedJSON ).toHaveKeyWithCase( "error" ); 67 | expect( returnedJSON.error ).toBeFalse(); 68 | expect( event ).toHaveStatusCode( 200 ); 69 | expect( returnedJSON ).toHaveKeyWithCase( "data" ); 70 | expect( returnedJSON.data ).toBeStruct(); 71 | expect( returnedJSON.data ).toHaveKeyWithCase( "id" ); 72 | expect( returnedJSON.data.id ).toBe( 7 ); 73 | expect( returnedJSON ).toHaveKeyWithCase( "messages" ); 74 | expect( returnedJSON.messages ).toBeArray(); 75 | expect( returnedJSON.messages ).toHaveLength( 0 ); 76 | } ); 77 | } ); 78 | } ); 79 | } ); 80 | 81 | 82 | scenario( "Create a Rant", function() { 83 | given( "I make a post call to /api/v5/rants", function() { 84 | when( "Using a get method", function() { 85 | then( "I will hit the index action instead of the create action", function() { 86 | var event = get( "/api/v5/rants" ); 87 | expect( event.getCurrentAction() ).toBe( 88 | "index", 89 | "Expected to hit index action not [#event.getCurrentAction()#] action" 90 | ); 91 | } ); 92 | } ); 93 | 94 | when( "Including no userID param", function() { 95 | then( "I will get a 412 error", function() { 96 | var event = post( "/api/v5/rants", {} ); 97 | var returnedJSON = event.getRenderData().data; 98 | expect( returnedJSON ).toHaveKeyWithCase( "error" ); 99 | expect( returnedJSON.error ).toBeTrue(); 100 | expect( event ).toHaveStatusCode( 412 ); 101 | expect( returnedJSON ).toHaveKeyWithCase( "messages" ); 102 | expect( returnedJSON.messages ).toBeArray(); 103 | expect( returnedJSON.messages ).toHaveLengthGTE( 1 ); 104 | expect( returnedJSON.messages[ 1 ] ).toBe( "The 'USERID' value is required" ); 105 | } ); 106 | } ); 107 | 108 | when( "Including an empty userID param", function() { 109 | then( "I will get a 412 error", function() { 110 | var event = post( "/api/v5/rants", { "userID": "" } ); 111 | var returnedJSON = event.getRenderData().data; 112 | expect( returnedJSON ).toHaveKeyWithCase( "error" ); 113 | expect( returnedJSON.error ).toBeTrue(); 114 | expect( event ).toHaveStatusCode( 412 ); 115 | expect( returnedJSON ).toHaveKeyWithCase( "messages" ); 116 | expect( returnedJSON.messages ).toBeArray(); 117 | expect( returnedJSON.messages ).toHaveLengthGTE( 1 ); 118 | expect( returnedJSON.messages[ 1 ] ).toBe( "The 'USERID' value is required" ); 119 | } ); 120 | } ); 121 | 122 | when( "Including a non numeric userID param", function() { 123 | then( "I will get a 412 error", function() { 124 | var event = post( "/api/v5/rants", { "userID": "abc" } ); 125 | var returnedJSON = event.getRenderData().data; 126 | expect( returnedJSON ).toHaveKeyWithCase( "error" ); 127 | expect( returnedJSON.error ).toBeTrue(); 128 | expect( event ).toHaveStatusCode( 412 ); 129 | expect( returnedJSON ).toHaveKeyWithCase( "messages" ); 130 | expect( returnedJSON.messages ).toBeArray(); 131 | expect( returnedJSON.messages ).toHaveLengthGTE( 1 ); 132 | expect( returnedJSON.messages[ 1 ] ).toBe( 133 | "The 'USERID' has an invalid type, expected type is numeric" 134 | ); 135 | } ); 136 | } ); 137 | 138 | when( "Including no body param", function() { 139 | then( "I will get a 412 error", function() { 140 | var event = post( "/api/v5/rants", { "userID": "5" } ); 141 | var returnedJSON = event.getRenderData().data; 142 | expect( returnedJSON ).toHaveKeyWithCase( "error" ); 143 | expect( returnedJSON.error ).toBeTrue(); 144 | expect( event ).toHaveStatusCode( 412 ); 145 | expect( returnedJSON ).toHaveKeyWithCase( "messages" ); 146 | expect( returnedJSON.messages ).toBeArray(); 147 | expect( returnedJSON.messages ).toHaveLengthGTE( 1 ); 148 | expect( returnedJSON.messages[ 1 ] ).toBe( "The 'BODY' value is required" ); 149 | } ); 150 | } ); 151 | 152 | when( "Including an empty body param", function() { 153 | then( "I will get a 412 error", function() { 154 | var event = post( "/api/v5/rants", { "userID": "5", "body": "" } ); 155 | var returnedJSON = event.getRenderData().data; 156 | expect( returnedJSON ).toHaveKeyWithCase( "error" ); 157 | expect( returnedJSON.error ).toBeTrue(); 158 | expect( event ).toHaveStatusCode( 412 ); 159 | expect( returnedJSON ).toHaveKeyWithCase( "messages" ); 160 | expect( returnedJSON.messages ).toBeArray(); 161 | expect( returnedJSON.messages ).toHaveLengthGTE( 1 ); 162 | expect( returnedJSON.messages[ 1 ] ).toBe( "The 'BODY' value is required" ); 163 | } ); 164 | } ); 165 | 166 | when( "Including valid userID for a non existing User", function() { 167 | then( "I will get a 404 error", function() { 168 | var event = post( "/api/v5/rants", { "body": "xsxswxws", "userID": "1" } ); 169 | var returnedJSON = event.getRenderData().data; 170 | expect( returnedJSON ).toHaveKeyWithCase( "error" ); 171 | expect( returnedJSON.error ).toBeTrue(); 172 | expect( event ).toHaveStatusCode( 404 ); 173 | expect( returnedJSON ).toHaveKeyWithCase( "messages" ); 174 | expect( returnedJSON.messages ).toBeArray(); 175 | expect( returnedJSON.messages ).toHaveLengthGTE( 1 ); 176 | expect( returnedJSON.messages[ 1 ] ).toBe( "User not found" ); 177 | } ); 178 | } ); 179 | 180 | when( "I pass a valid body and userID", function() { 181 | then( "I will get a successful query result with a generatedKey", function() { 182 | var event = post( "/api/v5/rants", { "body": "xsxswxws", "userID": "5" } ); 183 | var returnedJSON = event.getRenderData().data; 184 | expect( returnedJSON ).toHaveKeyWithCase( "error" ); 185 | expect( returnedJSON.error ).toBeFalse(); 186 | expect( event.getStatusCode() ).toBe( 200 ); 187 | expect( returnedJSON ).toHaveKeyWithCase( "data" ); 188 | expect( returnedJSON.data ).toBeStruct(); 189 | expect( returnedJSON.data ).toHaveKeyWithCase( "rantID" ); 190 | expect( returnedJSON.data.rantID ).toBeGT( 7 ); 191 | expect( returnedJSON ).toHaveKeyWithCase( "messages" ); 192 | expect( returnedJSON.messages ).toBeArray(); 193 | expect( returnedJSON.messages ).toHaveLengthGTE( 1 ); 194 | expect( returnedJSON.messages[ 1 ] ).toBe( "Rant Created" ); 195 | } ); 196 | } ); 197 | } ); 198 | } ); 199 | 200 | 201 | 202 | scenario( "Update a Rant", function() { 203 | given( "I make a get call to /api/v5/rants/:rantID", function() { 204 | when( "Using a get method", function() { 205 | then( "I will hit the show action instead of the update action", function() { 206 | var rantID = "1"; 207 | var event = get( "/api/v5/rants/#rantID#" ); 208 | expect( event.getCurrentAction() ).toBe( 209 | "show", 210 | "I expect to hit show action instead of the update action due to the VERB, but I actually hit [#event.getCurrentAction()#]" 211 | ); 212 | } ); 213 | } ); 214 | 215 | when( "Using a post method", function() { 216 | then( "I will hit the show action instead of the update action", function() { 217 | var rantID = "1"; 218 | var event = post( "/api/v5/rants/#rantID#" ); 219 | expect( event.getCurrentAction() ).toBe( 220 | "onInvalidHTTPMethod", 221 | "I expect to hit onInvalidHTTPMethod action instead of the update action due to the VERB, but I actually hit [#event.getCurrentAction()#]" 222 | ); 223 | } ); 224 | } ); 225 | 226 | when( "Including no userID param", function() { 227 | then( "I will get a 412 error", function() { 228 | var rantID = "7"; 229 | var event = put( "/api/v5/rants/#rantID#", {} ); 230 | var returnedJSON = event.getRenderData().data; 231 | expect( returnedJSON ).toHaveKeyWithCase( "error" ); 232 | expect( returnedJSON.error ).toBeTrue(); 233 | expect( event ).toHaveStatusCode( 412 ); 234 | expect( returnedJSON ).toHaveKeyWithCase( "messages" ); 235 | expect( returnedJSON.messages ).toBeArray(); 236 | expect( returnedJSON.messages ).toHaveLengthGTE( 1 ); 237 | expect( returnedJSON.messages[ 1 ] ).toBe( "The 'USERID' value is required" ); 238 | } ); 239 | } ); 240 | 241 | when( "Including an empty userID param", function() { 242 | then( "I will get a 412 error", function() { 243 | var rantID = "7"; 244 | var event = put( "/api/v5/rants/#rantID#", { "userID": "" } ); 245 | var returnedJSON = event.getRenderData().data; 246 | expect( returnedJSON ).toHaveKeyWithCase( "error" ); 247 | expect( returnedJSON.error ).toBeTrue(); 248 | expect( event ).toHaveStatusCode( 412 ); 249 | expect( returnedJSON ).toHaveKeyWithCase( "messages" ); 250 | expect( returnedJSON.messages ).toBeArray(); 251 | expect( returnedJSON.messages ).toHaveLengthGTE( 1 ); 252 | expect( returnedJSON.messages[ 1 ] ).toBe( "The 'USERID' value is required" ); 253 | } ); 254 | } ); 255 | 256 | when( "Including a non numeric userID param", function() { 257 | then( "I will get a 412 error", function() { 258 | var rantID = "7"; 259 | var event = put( "/api/v5/rants/#rantID#", { "userID": "abc" } ); 260 | var returnedJSON = event.getRenderData().data; 261 | expect( returnedJSON ).toHaveKeyWithCase( "error" ); 262 | expect( returnedJSON.error ).toBeTrue(); 263 | expect( event ).toHaveStatusCode( 412 ); 264 | expect( returnedJSON ).toHaveKeyWithCase( "messages" ); 265 | expect( returnedJSON.messages ).toBeArray(); 266 | expect( returnedJSON.messages ).toHaveLengthGTE( 1 ); 267 | expect( returnedJSON.messages[ 1 ] ).toBe( 268 | "The 'USERID' has an invalid type, expected type is numeric" 269 | ); 270 | } ); 271 | } ); 272 | 273 | 274 | when( "Including no body param", function() { 275 | then( "I will get a 412 error", function() { 276 | var rantID = "1"; 277 | var event = put( "/api/v5/rants/#rantID#", { "userID": "1" } ); 278 | var returnedJSON = event.getRenderData().data; 279 | expect( returnedJSON ).toHaveKeyWithCase( "error" ); 280 | expect( returnedJSON.error ).toBeTrue(); 281 | expect( event ).toHaveStatusCode( 412 ); 282 | expect( returnedJSON ).toHaveKeyWithCase( "messages" ); 283 | expect( returnedJSON.messages ).toBeArray(); 284 | expect( returnedJSON.messages ).toHaveLengthGTE( 1 ); 285 | expect( returnedJSON.messages[ 1 ] ).toBe( "The 'BODY' value is required" ); 286 | } ); 287 | } ); 288 | 289 | when( "Including an empty body param", function() { 290 | then( "I will get a 412 error", function() { 291 | var rantID = "1"; 292 | var event = put( "/api/v5/rants/#rantID#", { "userID": "1", "body": "" } ); 293 | var returnedJSON = event.getRenderData().data; 294 | expect( returnedJSON ).toHaveKeyWithCase( "error" ); 295 | expect( returnedJSON.error ).toBeTrue(); 296 | expect( event ).toHaveStatusCode( 412 ); 297 | expect( returnedJSON ).toHaveKeyWithCase( "messages" ); 298 | expect( returnedJSON.messages ).toBeArray(); 299 | expect( returnedJSON.messages ).toHaveLengthGTE( 1 ); 300 | expect( returnedJSON.messages[ 1 ] ).toBe( "The 'BODY' value is required" ); 301 | } ); 302 | } ); 303 | 304 | when( "Including a non numeric rantID param", function() { 305 | then( "I will get a 412 error", function() { 306 | var rantID = "abc"; 307 | var event = put( "/api/v5/rants/#rantID#", { "userID": "1", "body": "abc" } ); 308 | var returnedJSON = event.getRenderData().data; 309 | expect( returnedJSON ).toHaveKeyWithCase( "error" ); 310 | expect( returnedJSON.error ).toBeTrue(); 311 | expect( event ).toHaveStatusCode( 412 ); 312 | expect( returnedJSON ).toHaveKeyWithCase( "messages" ); 313 | expect( returnedJSON.messages ).toBeArray(); 314 | expect( returnedJSON.messages ).toHaveLengthGTE( 1 ); 315 | expect( returnedJSON.messages[ 1 ] ).toBe( 316 | "The 'RANTID' has an invalid type, expected type is numeric" 317 | ); 318 | } ); 319 | } ); 320 | 321 | when( "Including valid userID for a non existing User", function() { 322 | then( "I will get a 404 error", function() { 323 | var rantID = "7"; 324 | var event = put( "/api/v5/rants/#rantID#", { "body": "xsxswxws", "userID": "1" } ); 325 | var returnedJSON = event.getRenderData().data; 326 | expect( returnedJSON ).toHaveKeyWithCase( "error" ); 327 | expect( returnedJSON.error ).toBeTrue(); 328 | expect( event ).toHaveStatusCode( 404 ); 329 | expect( returnedJSON ).toHaveKeyWithCase( "messages" ); 330 | expect( returnedJSON.messages ).toBeArray(); 331 | expect( returnedJSON.messages ).toHaveLengthGTE( 1 ); 332 | expect( returnedJSON.messages[ 1 ] ).toBe( "User not found" ); 333 | } ); 334 | } ); 335 | 336 | when( "Including valid rantID for a non existing Rant", function() { 337 | then( "I will get a 404 error", function() { 338 | var rantID = "1"; 339 | var event = put( "/api/v5/rants/#rantID#", { "userID": "5", "body": "xsxswxws" } ); 340 | var returnedJSON = event.getRenderData().data; 341 | expect( returnedJSON ).toHaveKeyWithCase( "error" ); 342 | expect( returnedJSON.error ).toBeTrue(); 343 | expect( event ).toHaveStatusCode( 404 ); 344 | expect( returnedJSON ).toHaveKeyWithCase( "messages" ); 345 | expect( returnedJSON.messages ).toBeArray(); 346 | expect( returnedJSON.messages ).toHaveLengthGTE( 1 ); 347 | expect( returnedJSON.messages[ 1 ] ).toBe( "Rant not found" ); 348 | } ); 349 | } ); 350 | 351 | when( "I pass a valid body and userID and rantID", function() { 352 | then( "I will update the Rant Successfully", function() { 353 | var rantID = "7"; 354 | var event = put( "/api/v5/rants/#rantID#", { "body": "xsxswxws", "userID": "5" } ); 355 | var returnedJSON = event.getRenderData().data; 356 | expect( returnedJSON ).toHaveKeyWithCase( "error" ); 357 | expect( returnedJSON.error ).toBeFalse(); 358 | expect( event.getStatusCode() ).toBe( 200 ); 359 | expect( returnedJSON ).toHaveKeyWithCase( "data" ); 360 | expect( returnedJSON ).toHaveKeyWithCase( "messages" ); 361 | expect( returnedJSON.messages ).toBeArray(); 362 | expect( returnedJSON.messages ).toHaveLengthGTE( 1 ); 363 | expect( returnedJSON.messages[ 1 ] ).toBe( "Rant Updated" ); 364 | } ); 365 | } ); 366 | } ); 367 | } ); 368 | 369 | 370 | scenario( "Delete a Rant", function() { 371 | given( "I make a get call to /api/v5/rants/:rantID", function() { 372 | when( "Using a get method", function() { 373 | then( "I will hit the show action instead of the update action", function() { 374 | var rantID = "1"; 375 | var event = get( "/api/v5/rants/#rantID#" ); 376 | expect( event.getCurrentAction() ).toBe( 377 | "show", 378 | "I expect to hit show action instead of the delete action due to the VERB, but I actually hit [#event.getCurrentAction()#]" 379 | ); 380 | } ); 381 | } ); 382 | 383 | when( "Using a post method", function() { 384 | then( "I will hit the show action instead of the update action", function() { 385 | var rantID = "1"; 386 | var event = post( "/api/v5/rants/#rantID#" ); 387 | expect( event.getCurrentAction() ).toBe( 388 | "onInvalidHTTPMethod", 389 | "I expect to hit onInvalidHTTPMethod action instead of the delete action due to the VERB, but I actually hit [#event.getCurrentAction()#]" 390 | ); 391 | } ); 392 | } ); 393 | 394 | when( "Including a space for rantID param", function() { 395 | then( "I will hit the index action instead of the delete action", function() { 396 | var rantID = " "; 397 | var event = get( "/api/v5/rants/#rantID#" ); 398 | expect( event.getCurrentAction() ).toBe( 399 | "index", 400 | "I expect to hit index action instead of the delete action due to the VERB, but I actually hit [#event.getCurrentAction()#]" 401 | ); 402 | } ); 403 | } ); 404 | 405 | when( "Including a non numeric rantID param", function() { 406 | then( "I will get a 412 error", function() { 407 | var rantID = "abc"; 408 | var event = delete( "/api/v5/rants/#rantID#" ); 409 | var returnedJSON = event.getRenderData().data; 410 | expect( returnedJSON ).toHaveKeyWithCase( "error" ); 411 | expect( returnedJSON.error ).toBeTrue(); 412 | expect( event ).toHaveStatusCode( 412 ); 413 | expect( returnedJSON ).toHaveKeyWithCase( "messages" ); 414 | expect( returnedJSON.messages ).toBeArray(); 415 | expect( returnedJSON.messages ).toHaveLengthGTE( 1 ); 416 | expect( returnedJSON.messages[ 1 ] ).toBe( 417 | "The 'RANTID' has an invalid type, expected type is numeric" 418 | ); 419 | } ); 420 | } ); 421 | 422 | when( "Including valid rantID for a non existing Rant", function() { 423 | then( "I will get a 404 error", function() { 424 | var rantID = 1; 425 | var event = delete( "/api/v5/rants/#rantID#" ); 426 | var returnedJSON = event.getRenderData().data; 427 | expect( returnedJSON ).toHaveKeyWithCase( "error" ); 428 | expect( returnedJSON.error ).toBeTrue(); 429 | expect( event ).toHaveStatusCode( 404 ); 430 | expect( returnedJSON ).toHaveKeyWithCase( "messages" ); 431 | expect( returnedJSON.messages ).toBeArray(); 432 | expect( returnedJSON.messages ).toHaveLengthGTE( 1 ); 433 | expect( returnedJSON.messages[ 1 ] ).toBe( "Rant not found" ); 434 | } ); 435 | } ); 436 | 437 | when( "I pass a valid rantID", function() { 438 | then( "I will delete the rant successfully", function() { 439 | var event = post( "/api/v5/rants", { "body": "New Rant Created to Delete", "userID": "5" } ); 440 | var returnedJSON = event.getRenderData().data; 441 | setup(); 442 | var rantID = returnedJSON.data.rantID; 443 | var event2 = delete( "/api/v5/rants/#rantID#" ); 444 | var returnedJSON2 = event2.getRenderData().data; 445 | debug( returnedJSON2 ); 446 | expect( returnedJSON2 ).toHaveKeyWithCase( "error" ); 447 | expect( returnedJSON2.error ).toBeFalse(); 448 | expect( event2.getStatusCode() ).toBe( 200 ); 449 | expect( returnedJSON2 ).toHaveKeyWithCase( "data" ); 450 | expect( returnedJSON2 ).toHaveKeyWithCase( "messages" ); 451 | expect( returnedJSON2.messages ).toBeArray(); 452 | expect( returnedJSON2.messages ).toHaveLengthGTE( 1 ); 453 | expect( returnedJSON2.messages[ 1 ] ).toBe( "Rant Deleted" ); 454 | } ); 455 | } ); 456 | } ); 457 | } ); 458 | } ); 459 | } 460 | 461 | } 462 | -------------------------------------------------------------------------------- /tests/test.xml: -------------------------------------------------------------------------------- 1 | 2 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | Tests ran at ${start.TODAY} 50 | 51 | 52 | 53 | 54 | 64 | 67 | 68 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 102 | 105 | 106 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /workbench/database/20200507_fluentapi.sql: -------------------------------------------------------------------------------- 1 | -- -------------------------------------------------------- 2 | -- Host: 127.0.0.1 3 | -- Server version: 5.7.12 - MySQL Community Server (GPL) 4 | -- Server OS: Win64 5 | -- HeidiSQL Version: 11.0.0.5919 6 | -- -------------------------------------------------------- 7 | 8 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; 9 | /*!40101 SET NAMES utf8 */; 10 | /*!50503 SET NAMES utf8mb4 */; 11 | /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; 12 | /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; 13 | 14 | -- Dumping structure for table fluentapi.rants 15 | CREATE TABLE IF NOT EXISTS `rants` ( 16 | `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 17 | `body` text NOT NULL, 18 | `createdDate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 19 | `modifiedDate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 20 | `userId` int(10) unsigned NOT NULL, 21 | PRIMARY KEY (`id`), 22 | KEY `fk_rants_userId` (`userId`), 23 | CONSTRAINT `fk_rants_userId` FOREIGN KEY (`userId`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE 24 | ) ENGINE=InnoDB AUTO_INCREMENT=152 DEFAULT CHARSET=latin1; 25 | 26 | -- Dumping data for table fluentapi.rants: ~23 rows (approximately) 27 | /*!40000 ALTER TABLE `rants` DISABLE KEYS */; 28 | INSERT INTO `rants` (`id`, `body`, `createdDate`, `modifiedDate`, `userId`) VALUES 29 | (4, 'Another rant', '2020-05-03 23:00:07', '2020-05-03 23:00:07', 2), 30 | (6, 'Another rant, where\'s my soapbox2', '2020-05-03 23:07:39', '2020-05-04 11:22:48', 3), 31 | (7, 'Another rant, where\'s my soapbox23', '2020-05-03 23:20:22', '2020-05-07 11:43:07', 2), 32 | (8, 'Another rant, where\'s my soapbox2', '2020-05-04 11:21:31', '2020-05-04 11:21:31', 2), 33 | (9, 'Another rant, where\'s my soapbox2', '2020-05-04 12:05:18', '2020-05-04 12:05:18', 2), 34 | (10, 'Another rant, where\'s my soapbox2', '2020-05-04 12:05:44', '2020-05-04 12:05:44', 2), 35 | (11, 'Another rant, where\'s my soapbox2', '2020-05-04 20:58:28', '2020-05-04 20:58:28', 2), 36 | (12, 'Another rant, where\'s my soapbox2', '2020-05-04 20:59:44', '2020-05-04 20:59:44', 2), 37 | (13, 'Another rant, where\'s my soapbox2', '2020-05-04 21:06:16', '2020-05-04 21:06:16', 2), 38 | (14, 'Another rant, where\'s my soapbox2', '2020-05-04 21:09:03', '2020-05-04 21:09:03', 2), 39 | (15, 'Another rant, where\'s my soapbox2', '2020-05-04 21:09:13', '2020-05-04 21:09:13', 2), 40 | (16, 'Another rant, where\'s my soapbox2', '2020-05-04 21:15:10', '2020-05-04 21:15:10', 2), 41 | (18, 'Another rant, where\'s my soapbox2', '2020-05-07 09:29:06', '2020-05-07 09:29:06', 2), 42 | (19, 'Another rant, where\'s my soapbox2', '2020-05-07 09:30:38', '2020-05-07 09:30:38', 2), 43 | (20, 'Another rant, where\'s my soapbox2', '2020-05-07 09:30:46', '2020-05-07 09:30:46', 2), 44 | (23, 'Another rant, where\'s my soapbox2', '2020-05-07 09:36:04', '2020-05-07 09:36:04', 2), 45 | (24, 'Another rant, where\'s my soapbox2', '2020-05-07 09:37:03', '2020-05-07 09:37:03', 2), 46 | (25, 'Another rant, where\'s my soapbox2', '2020-05-07 09:46:15', '2020-05-07 09:46:15', 2), 47 | (110, 'Testing test test', '2020-05-07 22:18:43', '2020-05-07 22:18:43', 2), 48 | (111, 'Testing test test', '2020-05-07 22:19:31', '2020-05-07 22:19:31', 2), 49 | (112, 'Scott likes me preso', '2020-05-07 22:19:37', '2020-05-07 22:19:37', 5), 50 | (113, 'Scott seems to like my preso', '2020-05-07 22:20:32', '2020-05-07 22:20:32', 2); 51 | /*!40000 ALTER TABLE `rants` ENABLE KEYS */; 52 | 53 | -- Dumping structure for table fluentapi.users 54 | CREATE TABLE IF NOT EXISTS `users` ( 55 | `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 56 | `username` varchar(255) NOT NULL, 57 | `email` varchar(255) NOT NULL, 58 | `password` varchar(255) NOT NULL, 59 | `createdDate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 60 | `modifiedDate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 61 | PRIMARY KEY (`id`), 62 | UNIQUE KEY `username` (`username`), 63 | UNIQUE KEY `email` (`email`) 64 | ) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=latin1; 65 | 66 | -- Dumping data for table fluentapi.users: ~6 rows (approximately) 67 | /*!40000 ALTER TABLE `users` DISABLE KEYS */; 68 | INSERT INTO `users` (`id`, `username`, `email`, `password`, `createdDate`, `modifiedDate`) VALUES 69 | (2, 'gpickin', 'gavin@ortussolutions.com', '$2a$12$JKiBJZF352Tfm/c3PpeslOBKRAwtXlwczMPKeUV1raD0d1cwh5B5.', '2018-10-04 17:55:19', '2018-10-04 17:55:19'), 70 | (3, 'luis', 'lmajano@ortussolutions.com', '$2a$12$kSM/7Q5WgJ/xaKfLYwbPj.4QVJZo7tonT/h/PFDoUwfW3GDV/AttC', '2018-10-05 09:07:14', '2018-10-05 09:07:14'), 71 | (4, 'brad', 'brad@ortussolutions.com', '$2a$12$Vbb4dYywI5X.1qKEV2mDzeOTZk3iHIDfEtz80SoMT0KkFWTkb.PB6', '2018-10-05 09:29:37', '2018-10-05 09:29:37'), 72 | (5, 'javier', 'jquintero@ortussolutions.com', '$2a$12$UIEOglSflvGUbn5sHeBZ1.sAlaoBI4rpNOCIk2vF8R2KKz.ihP9/W', '2018-10-05 09:30:32', '2018-10-05 09:30:32'), 73 | (6, 'scott', 'scott@scott.com', '$2a$12$OjIpxecG9AlZTgVGV1jsvOegTwbqgJ29PlUkfomGsK/6hsVicsRW.', '2018-10-05 09:32:07', '2018-10-05 09:32:07'), 74 | (7, 'mike', 'mikep@netxn.com', '$2a$12$WWUwFEAoDGx.vB0jE54xser1myMUSwUMYo/aNn0cSGa8l6DQe67Q2', '2018-10-05 09:33:00', '2018-10-05 09:33:00'); 75 | /*!40000 ALTER TABLE `users` ENABLE KEYS */; 76 | 77 | /*!40101 SET SQL_MODE=IFNULL(@OLD_SQL_MODE, '') */; 78 | /*!40014 SET FOREIGN_KEY_CHECKS=IF(@OLD_FOREIGN_KEY_CHECKS IS NULL, 1, @OLD_FOREIGN_KEY_CHECKS) */; 79 | /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; 80 | --------------------------------------------------------------------------------