├── .cfconfig.json ├── .cfformat.json ├── .cflintrc ├── .cfmigrations.json ├── .editorconfig ├── .env.example ├── .gitattributes ├── .github ├── FUNDING.YML └── workflows │ ├── ci.yml │ ├── pr.yml │ └── tests.yml ├── .gitignore ├── .gitlab-ci.yml ├── .vscode └── settings.json ├── Application.cfc ├── Dockerfile ├── box.json ├── config ├── Application.cfc ├── CacheBox.cfc ├── Coldbox.cfc ├── Router.cfc ├── Scheduler.cfc ├── WireBox.cfc └── modules │ ├── cbdebugger.cfc │ ├── cbswagger.cfc │ └── mementifier.cfc ├── index.cfm ├── modules_app └── api │ ├── ModuleConfig.cfc │ ├── config │ └── Router.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 │ │ ├── User.cfc │ │ └── UserService.cfc │ ├── v5 │ ├── ModuleConfig.cfc │ ├── config │ │ └── Router.cfc │ ├── handlers │ │ ├── Echo.cfc │ │ └── Rants.cfc │ └── models │ │ ├── BaseEntity.cfc │ │ ├── BaseService.cfc │ │ ├── Rant.cfc │ │ ├── RantService.cfc │ │ ├── User.cfc │ │ └── UserService.cfc │ └── v6 │ ├── ModuleConfig.cfc │ ├── config │ └── Router.cfc │ ├── handlers │ ├── Echo.cfc │ └── Rants.cfc │ └── models │ ├── BaseEntity.cfc │ ├── BaseService.cfc │ ├── Rant.cfc │ ├── RantService.cfc │ ├── User.cfc │ └── UserService.cfc ├── readme.md ├── resources ├── apidocs │ ├── _responses │ │ ├── rant.404.json │ │ └── user.404.json │ └── api-v6 │ │ └── Rants │ │ ├── create │ │ ├── example.200.json │ │ ├── example.400.json │ │ ├── requestBody.json │ │ └── responses.json │ │ ├── delete │ │ ├── example.200.json │ │ ├── parameters.json │ │ └── responses.json │ │ ├── index │ │ ├── example.200.json │ │ └── responses.json │ │ ├── show │ │ ├── example.200.json │ │ ├── parameters.json │ │ └── responses.json │ │ └── update │ │ ├── example.200.json │ │ ├── example.400.json │ │ ├── parameters.json │ │ ├── requestBody.json │ │ └── responses.json └── database │ └── migrations │ ├── 2020_05_15_183916_users.cfc │ ├── 2020_05_15_183939_rants.cfc │ └── 2020_05_15_184033_seedrants.cfc ├── server-adobe.json ├── server-boxlang.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 └── fluentapi.sql └── setup-env.sh /.cfconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "systemErr":"System", 3 | "systemOut":"System", 4 | "componentCacheEnabled":false, 5 | "thistimezone":"UTC", 6 | "adminPassword": "${CFCONFIG_ADMINPASSWORD}", 7 | "debuggingEnabled":true, 8 | "debuggingShowDatabase":true, 9 | "debuggingReportExecutionTimes": false, 10 | "disableInternalCFJavaComponents":false, 11 | "requestTimeoutEnabled": true, 12 | "robustExceptionEnabled": true, 13 | "whitespaceManagement": "white-space-pref", 14 | "requestTimeout": "0,0,5,0", 15 | "datasources": { 16 | "${DB_DATABASE}": { 17 | "bundleName": "${DB_BUNDLENAME}", 18 | "bundleVersion": "${DB_BUNDLEVERSION}", 19 | "class": "${DB_CLASS}", 20 | "custom": "useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&useLegacyDatetimeCode=true&allowPublicKeyRetrieval=true", 21 | "database": "${DB_DATABASE}", 22 | "dbdriver": "MySQL", 23 | "dsn": "jdbc:mysql://{host}:{port}/{database}", 24 | "host":"${DB_HOST:127.0.0.1}", 25 | "password": "${DB_PASSWORD}", 26 | "port": "${DB_PORT:3306}", 27 | "username": "${DB_USER:root}", 28 | "storage":"false", 29 | "validate":"false" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.cfformat.json: -------------------------------------------------------------------------------- 1 | { 2 | "array.empty_padding": false, 3 | "array.padding": true, 4 | "array.multiline.min_length": 50, 5 | "array.multiline.element_count": 2, 6 | "array.multiline.leading_comma.padding": true, 7 | "array.multiline.leading_comma": false, 8 | "alignment.consecutive.assignments": true, 9 | "alignment.consecutive.properties": true, 10 | "alignment.consecutive.params": true, 11 | "alignment.doc_comments" : true, 12 | "brackets.padding": true, 13 | "comment.asterisks": "align", 14 | "binary_operators.padding": true, 15 | "for_loop_semicolons.padding": true, 16 | "function_call.empty_padding": false, 17 | "function_call.padding": true, 18 | "function_call.multiline.leading_comma.padding": true, 19 | "function_call.casing.builtin": "cfdocs", 20 | "function_call.casing.userdefined": "camel", 21 | "function_call.multiline.element_count": 3, 22 | "function_call.multiline.leading_comma": false, 23 | "function_call.multiline.min_length": 50, 24 | "function_declaration.padding": true, 25 | "function_declaration.empty_padding": false, 26 | "function_declaration.multiline.leading_comma": false, 27 | "function_declaration.multiline.leading_comma.padding": true, 28 | "function_declaration.multiline.element_count": 3, 29 | "function_declaration.multiline.min_length": 50, 30 | "function_declaration.group_to_block_spacing": "compact", 31 | "function_anonymous.empty_padding": false, 32 | "function_anonymous.group_to_block_spacing": "compact", 33 | "function_anonymous.multiline.element_count": 3, 34 | "function_anonymous.multiline.leading_comma": false, 35 | "function_anonymous.multiline.leading_comma.padding": true, 36 | "function_anonymous.multiline.min_length": 50, 37 | "function_anonymous.padding": true, 38 | "indent_size": 4, 39 | "keywords.block_to_keyword_spacing": "spaced", 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 | "keywords.empty_group_spacing": false, 45 | "max_columns": 115, 46 | "metadata.multiline.element_count": 3, 47 | "metadata.multiline.min_length": 50, 48 | "method_call.chain.multiline" : 3, 49 | "newline":"\n", 50 | "property.multiline.element_count": 3, 51 | "property.multiline.min_length": 30, 52 | "parentheses.padding": true, 53 | "strings.quote": "double", 54 | "strings.attributes.quote": "double", 55 | "struct.separator": " : ", 56 | "struct.padding": true, 57 | "struct.empty_padding": false, 58 | "struct.multiline.leading_comma": false, 59 | "struct.multiline.leading_comma.padding": true, 60 | "struct.multiline.element_count": 2, 61 | "struct.multiline.min_length": 60, 62 | "tab_indent": true 63 | } 64 | -------------------------------------------------------------------------------- /.cflintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rule": [], 3 | "includes": [ 4 | { "code": "AVOID_USING_CFINCLUDE_TAG" }, 5 | { "code": "AVOID_USING_CFABORT_TAG" }, 6 | { "code": "AVOID_USING_CFEXECUTE_TAG" }, 7 | { "code": "AVOID_USING_DEBUG_ATTR" }, 8 | { "code": "AVOID_USING_ABORT" }, 9 | { "code": "AVOID_USING_ISDATE" }, 10 | { "code": "AVOID_USING_ISDEBUGMODE" }, 11 | { "code": "AVOID_USING_CFINSERT_TAG" }, 12 | { "code": "AVOID_USING_CFUPDATE_TAG" }, 13 | { "code": "ARG_VAR_CONFLICT" }, 14 | { "code": "ARG_VAR_MIXED" }, 15 | { "code": "ARG_HINT_MISSING" }, 16 | { "code": "ARG_HINT_MISSING_SCRIPT" }, 17 | { "code" : "ARGUMENT_INVALID_NAME" }, 18 | { "code" : "ARGUMENT_ALLCAPS_NAME" }, 19 | { "code" : "ARGUMENT_TOO_WORDY" }, 20 | { "code" : "ARGUMENT_IS_TEMPORARY" }, 21 | { "code": "CFQUERYPARAM_REQ" }, 22 | { "code": "COMPARE_INSTEAD_OF_ASSIGN" }, 23 | { "code": "COMPONENT_HINT_MISSING" }, 24 | { "code" : "COMPONENT_INVALID_NAME" }, 25 | { "code" : "COMPONENT_ALLCAPS_NAME" }, 26 | { "code" : "COMPONENT_TOO_SHORT" }, 27 | { "code" : "COMPONENT_TOO_LONG" }, 28 | { "code" : "COMPONENT_TOO_WORDY" }, 29 | { "code" : "COMPONENT_IS_TEMPORARY" }, 30 | { "code" : "COMPONENT_HAS_PREFIX_OR_POSTFIX" }, 31 | { "code": "COMPLEX_BOOLEAN_CHECK" }, 32 | { "code": "EXCESSIVE_FUNCTION_LENGTH" }, 33 | { "code": "EXCESSIVE_COMPONENT_LENGTH" }, 34 | { "code": "EXCESSIVE_ARGUMENTS" }, 35 | { "code": "EXCESSIVE_FUNCTIONS" }, 36 | { "code": "EXPLICIT_BOOLEAN_CHECK" }, 37 | { "code": "FUNCTION_TOO_COMPLEX" }, 38 | { "code": "FUNCTION_HINT_MISSING" }, 39 | { "code": "FILE_SHOULD_START_WITH_LOWERCASE" }, 40 | { "code": "LOCAL_LITERAL_VALUE_USED_TOO_OFTEN" }, 41 | { "code": "GLOBAL_LITERAL_VALUE_USED_TOO_OFTEN" }, 42 | { "code": "MISSING_VAR" }, 43 | { "code" : "METHOD_INVALID_NAME" }, 44 | { "code" : "METHOD_ALLCAPS_NAME" }, 45 | { "code" : "METHOD_IS_TEMPORARY" }, 46 | { "code": "NESTED_CFOUTPUT" }, 47 | { "code": "NEVER_USE_QUERY_IN_CFM" }, 48 | { "code": "OUTPUT_ATTR" }, 49 | { "code" : "QUERYPARAM_REQ" }, 50 | { "code": "UNUSED_LOCAL_VARIABLE" }, 51 | { "code": "UNUSED_METHOD_ARGUMENT" }, 52 | { "code": "SQL_SELECT_STAR" }, 53 | { "code": "SCOPE_ALLCAPS_NAME" }, 54 | { "code": "VAR_ALLCAPS_NAME" }, 55 | { "code": "VAR_INVALID_NAME" }, 56 | { "code": "VAR_TOO_WORDY" } 57 | ], 58 | "inheritParent": false, 59 | "parameters": { 60 | "TooManyFunctionsChecker.maximum" : 20 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /.cfmigrations.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": { 3 | "manager": "cfmigrations.models.QBMigrationManager", 4 | "migrationsDirectory": "resources/database/migrations/", 5 | "seedsDirectory": "resources/database/seeds/", 6 | "properties": { 7 | "defaultGrammar": "AutoDiscover@qb", 8 | "schema": "${DB_DATABASE}", 9 | "migrationsTable": "cfmigrations", 10 | "connectionInfo": { 11 | "connectionString": "${DB_CONNECTIONSTRING}", 12 | "class": "${DB_CLASS}", 13 | "username": "${DB_USER}", 14 | "password": "${DB_PASSWORD}", 15 | "bundleName": "${DB_BUNDLENAME}", 16 | "bundleVersion": "${DB_BUNDLEVERSION}" 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.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 21 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | ################################################################# 2 | # App Name and Environment 3 | ################################################################# 4 | APPNAME=fluentAPI 5 | # This can be development, staging, production or custom. 6 | ENVIRONMENT=development 7 | # The password for the CFML Engine Administrator 8 | CFCONFIG_ADMINPASSWORD=fluentapi 9 | # The ColdBox Reinit password 10 | COLDBOX_REINITPASSWORD= 11 | # Development CBDebugger 12 | CBDEBUGGER_ENABLED=true 13 | 14 | ###################################################### 15 | # MySQL 8+ DB Driver 16 | ###################################################### 17 | #DB_BUNDLENAME=com.mysql.cj 18 | #DB_BUNDLEVERSION=8.0.19 19 | #DB_CLASS=com.mysql.cj.jdbc.Driver 20 | 21 | ###################################################### 22 | # MySQL 5.7 DB Driver 23 | ###################################################### 24 | DB_BUNDLENAME=com.mysql.jdbc 25 | DB_BUNDLEVERSION=5.1.40 26 | DB_CLASS=com.mysql.jdbc.Driver 27 | 28 | ###################################################### 29 | # MySQL * Settings 30 | ###################################################### 31 | DB_CONNECTIONSTRING=jdbc:mysql://127.0.0.1:3306/fluentapi?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&useLegacyDatetimeCode=true&allowPublicKeyRetrieval=true 32 | DB_HOST=127.0.0.1 33 | DB_PORT=3306 34 | DB_DATABASE=fluentapi 35 | DB_USER= 36 | DB_PASSWORD= 37 | 38 | ################################################################# 39 | # JWT Information 40 | # -------------------------------- 41 | # You can seed the JWT secret below or you can also leave it empty 42 | # and ContentBox will auto-generate one for you that rotates 43 | # everytime the application restarts or expires 44 | ################################################################# 45 | JWT_SECRET= 46 | 47 | ################################################################# 48 | # AWS S3 or Digital Ocean Spaces 49 | # -------------------------------- 50 | # If you are using any of our S3/Spaces compatible storages, you 51 | # will have to seed your credentials and information below 52 | ################################################################# 53 | S3_ACCESS_KEY= 54 | S3_SECRET_KEY= 55 | S3_REGION=us-east-1 56 | S3_DOMAIN=amazonaws.com 57 | S3_BUCKET= 58 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /.github/FUNDING.YML: -------------------------------------------------------------------------------- 1 | patreon: ortussolutions 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Modern Fluent API CI 2 | 3 | # Only on Development we build snapshots 4 | on: 5 | push: 6 | branches: 7 | - development 8 | - master 9 | 10 | jobs: 11 | ############################################# 12 | # Tests First baby! We fail, no build :( 13 | ############################################# 14 | tests: 15 | uses: ./.github/workflows/tests.yml 16 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: Pull Requests 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - development 7 | 8 | jobs: 9 | tests: 10 | uses: lmajano/modern-functional-fluent-cfml-rest/.github/workflows/tests.yml@development 11 | 12 | # Format PR 13 | format: 14 | name: Format 15 | runs-on: ubuntu-20.04 16 | steps: 17 | - name: Checkout Repository 18 | uses: actions/checkout@v2 19 | 20 | - uses: Ortus-Solutions/commandbox-action@v1.0.2 21 | with: 22 | cmd: run-script format 23 | 24 | - name: Commit Format Changes 25 | uses: stefanzweifel/git-auto-commit-action@v4 26 | with: 27 | commit_message: Apply cfformat changes 28 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Test Suites 2 | 3 | # We are a reusable Workflow only 4 | on: 5 | workflow_call: 6 | 7 | jobs: 8 | tests: 9 | name: Tests 10 | runs-on: ubuntu-20.04 11 | env: 12 | DB_USER: root 13 | DB_PASSWORD: root 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | cfengine: [ "lucee", "adobe" ] 18 | steps: 19 | - name: Checkout Repository 20 | uses: actions/checkout@v2 21 | 22 | - name: Setup Java 23 | uses: actions/setup-java@v2 24 | with: 25 | distribution: "adopt" 26 | java-version: "11" 27 | 28 | - name: Setup Database and Fixtures 29 | run: | 30 | sudo systemctl start mysql.service 31 | # Create Database 32 | mysql -u${{ env.DB_USER }} -p${{ env.DB_PASSWORD }} -e 'CREATE DATABASE fluentapi;' 33 | # Import Database 34 | mysql -u${{ env.DB_USER }} -p${{ env.DB_PASSWORD }} < workbench/database/fluentapi.sql 35 | 36 | - name: Setup Environment For Testing Process 37 | run: | 38 | # Setup .env 39 | touch .env 40 | # ENV 41 | printf "ENVIRONMENT=development\n" >> .env 42 | printf "DB_HOST=localhost\n" >> .env 43 | printf "DB_DATABASE=fluentapi\n" >> .env 44 | printf "DB_USER=${{ env.DB_USER }}\n" >> .env 45 | printf "DB_PASSWORD=${{ env.DB_PASSWORD }}\n" >> .env 46 | printf "DB_CLASS=com.mysql.cj.jdbc.Driver\n" >> .env 47 | printf "DB_BUNDLEVERSION=8.0.19\n" >> .env 48 | printf "DB_BUNDLENAME=com.mysql.cj\n" >> .env 49 | printf "CBDEBUGGER_ENABLED=false\n" >> .env 50 | 51 | - name: Setup CommandBox CLI 52 | uses: Ortus-Solutions/setup-commandbox@main 53 | 54 | - name: Install Dependencies 55 | run: | 56 | box install 57 | 58 | - name: Start ${{ matrix.cfengine }} Server 59 | run: | 60 | box server start serverConfigFile="server-${{ matrix.cfengine }}.json" --noSaveSettings --debug 61 | # Install Adobe 2021 cfpm modules 62 | if [[ "${{ matrix.cfengine }}" == "adobe@2021" ]] ; then 63 | box run-script install:2021 64 | fi 65 | curl http://127.0.0.1:60146 66 | 67 | - name: Run Tests 68 | run: | 69 | mkdir -p tests/results 70 | box testbox run --verbose outputFile=tests/results/test-results outputFormats=json,antjunit 71 | 72 | - name: Publish Test Results 73 | uses: EnricoMi/publish-unit-test-result-action@v1 74 | if: always() 75 | with: 76 | files: tests/results/**/*.xml 77 | check_name: "${{ matrix.cfengine }} Test Results" 78 | 79 | - name: Upload Test Results to Artifacts 80 | if: always() 81 | uses: actions/upload-artifact@v2 82 | with: 83 | name: test-results-${{ matrix.cfengine }} 84 | path: | 85 | tests/results/**/* 86 | 87 | - name: Failure Debugging Log 88 | if: ${{ failure() }} 89 | run: | 90 | box server log serverConfigFile="server-${{ matrix.cfengine }}.json" 91 | 92 | - name: Upload Debugging Log To Artifacts 93 | if: ${{ failure() }} 94 | uses: actions/upload-artifact@v2 95 | with: 96 | name: Failure Debugging Info - ${{ matrix.cfengine }} 97 | path: | 98 | .engine/**/logs/* 99 | .engine/**/WEB-INF/cfusion/logs/* 100 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # General Ignores + IDE 2 | .DS_Store 3 | settings.xml 4 | WEB-INF 5 | .env 6 | .engine/** 7 | bin/** 8 | grapher/** 9 | 10 | # logs + tests 11 | logs/** 12 | tests/results/** 13 | 14 | # npm 15 | **/node_modules/* 16 | npm-debug.log 17 | yarn-error.log 18 | 19 | ## Ignored Dependencies 20 | coldbox/* 21 | testbox/* 22 | modules/* 23 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: ortussolutions/commandbox 2 | 3 | stages: 4 | - tests 5 | - localStackCheck 6 | - imageBuild 7 | - remoteStackCheck 8 | - imageDeploy 9 | - imageTest 10 | - artifacts 11 | 12 | cfformat: 13 | image: ortussolutions/commandbox:latest 14 | stage: tests 15 | except: 16 | variables: 17 | - $CI_COMMIT_MESSAGE =~ /skip-format/ 18 | before_script: 19 | - echo 'skip before_script' 20 | script: 21 | - box install commandbox-cfformat 22 | - box run-script format:check 23 | 24 | 25 | create_staging_image: 26 | image: docker:19.03.8 27 | stage: imageBuild 28 | services: 29 | - docker:19.03.8-dind 30 | only: 31 | - development 32 | script: 33 | - docker run -v "${PWD}:/app" ortussolutions/commandbox box install --verbose 34 | - rm -rf .env* 35 | 36 | - echo "Logging in to Docker Hub Registry" 37 | - docker login -u gpickin -p $DOCKER_REGISTRY_PASSWORD 38 | #Build our compiled image using our Dockerfile 39 | - docker build --no-cache -t ${CI_COMMIT_REF_NAME} -f ./Dockerfile ./ 40 | - echo "Docker image successfully built" 41 | - docker tag ${CI_COMMIT_REF_NAME} gpickin/fluent-api:${CI_COMMIT_REF_NAME} 42 | - docker tag ${CI_COMMIT_REF_NAME} gpickin/fluent-api 43 | # Push our new image and tags to the registry 44 | - echo "Pushing new image to registry - gpickin/fluent-api:${CI_COMMIT_REF_NAME}" 45 | - docker push gpickin/fluent-api 46 | - docker push gpickin/fluent-api:${CI_COMMIT_REF_NAME} 47 | - echo "Image gpickin/fluent-api:${CI_COMMIT_REF_NAME} ( STAGING ) pushed successfully" 48 | 49 | 50 | deploy_staging: 51 | stage: imageDeploy 52 | only: 53 | - development 54 | before_script: 55 | - mkdir -p ~/.ssh 56 | - echo "${SSH_PRIVATE_KEY}" > ~/.ssh/id_rsa 57 | - chmod 600 ~/.ssh/id_rsa 58 | - touch ~/.ssh/known_hosts 59 | - apt-get update 60 | - apt-get upgrade -y 61 | - apt-get install -y ssh 62 | script: 63 | - export DOCKER_SERVICE_NAME=fluentAPIImage_cfml 64 | # Override our SSH connection point for staging of our swarm master - this may also be provided as an environment variable 65 | - export SSH_CONNECT=root@64.225.34.122 66 | # Connect up via SSH to avoid any prompts to add the host key 67 | - ssh -o 'StrictHostKeyChecking no' $SSH_CONNECT 68 | # Note: The --with-registry-auth flag must be passed in order for the master to pass its authentication to the registry to all nodes 69 | - ssh $SSH_CONNECT 70 | " 71 | sudo docker login -u gpickin -p $DOCKER_REGISTRY_PASSWORD && 72 | sudo docker service update 73 | --update-order start-first 74 | --detach=false 75 | --with-registry-auth 76 | --force 77 | --image gpickin/fluent-api ${DOCKER_SERVICE_NAME} 78 | " 79 | 80 | 81 | rollback_staging: 82 | stage: imageDeploy 83 | when: manual 84 | only: 85 | - development 86 | before_script: 87 | - mkdir -p ~/.ssh 88 | - echo "${SSH_PRIVATE_KEY}" > ~/.ssh/id_rsa 89 | - chmod 600 ~/.ssh/id_rsa 90 | - touch ~/.ssh/known_hosts 91 | script: 92 | - export DOCKER_SERVICE_NAME=fluentAPIImage_cfml 93 | # Override our SSH connection point for staging of our swarm master - this may also be provided as an environment variable 94 | - export SSH_CONNECT=root@64.225.34.122 95 | # Connect up via SSH to avoid any prompts to add the host key 96 | - ssh -o 'StrictHostKeyChecking no' $SSH_CONNECT 97 | - echo "Rolling Back STAGING" 98 | - ssh $SSH_CONNECT 99 | " 100 | sudo docker login -u gpickin -p $DOCKER_REGISTRY_PASSWORD && 101 | sudo docker service rollback --detach=false ${DOCKER_SERVICE_NAME} 102 | " -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cfml.mappings": [ 3 | { 4 | "logicalPath": "/coldbox", 5 | "directoryPath": "./coldbox", 6 | "isPhysicalDirectoryPath" :false 7 | }, 8 | { 9 | "logicalPath": "/testbox", 10 | "directoryPath": "./testbox", 11 | "isPhysicalDirectoryPath" :false 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /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 | 8 | // Application properties 9 | this.name = hash( getCurrentTemplatePath() ); 10 | this.sessionManagement = true; 11 | this.sessionTimeout = createTimespan( 0, 0, 30, 0 ); 12 | this.setClientCookies = true; 13 | this.datasource = "fluentAPI"; 14 | 15 | // Important 16 | this.serialization.preserveCaseForStructKey = true; 17 | this.serialization.preserveCaseForQueryColumn = true; 18 | 19 | // COLDBOX STATIC PROPERTY, DO NOT CHANGE UNLESS THIS IS NOT THE ROOT OF YOUR COLDBOX APP 20 | COLDBOX_APP_ROOT_PATH = getDirectoryFromPath( getCurrentTemplatePath() ); 21 | // The web server mapping to this application. Used for remote purposes or static purposes 22 | COLDBOX_APP_MAPPING = ""; 23 | // COLDBOX PROPERTIES 24 | COLDBOX_CONFIG_FILE = ""; 25 | // COLDBOX APPLICATION KEY OVERRIDE 26 | COLDBOX_APP_KEY = ""; 27 | 28 | // application start 29 | public boolean function onApplicationStart(){ 30 | application.cbBootstrap = new coldbox.system.Bootstrap( 31 | COLDBOX_CONFIG_FILE, 32 | COLDBOX_APP_ROOT_PATH, 33 | COLDBOX_APP_KEY, 34 | COLDBOX_APP_MAPPING 35 | ); 36 | application.cbBootstrap.loadColdbox(); 37 | return true; 38 | } 39 | 40 | // application end 41 | public boolean function onApplicationEnd( struct appScope ){ 42 | arguments.appScope.cbBootstrap.onApplicationEnd( arguments.appScope ); 43 | } 44 | 45 | // request start 46 | public boolean function onRequestStart( string targetPage ){ 47 | // Process ColdBox Request 48 | application.cbBootstrap.onRequestStart( arguments.targetPage ); 49 | 50 | return true; 51 | } 52 | 53 | public void function onSessionStart(){ 54 | application.cbBootStrap.onSessionStart(); 55 | } 56 | 57 | public void function onSessionEnd( struct sessionScope, struct appScope ){ 58 | arguments.appScope.cbBootStrap.onSessionEnd( argumentCollection = arguments ); 59 | } 60 | 61 | public boolean function onMissingTemplate( template ){ 62 | return application.cbBootstrap.onMissingTemplate( argumentCollection = arguments ); 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ortussolutions/commandbox 2 | COPY . /app 3 | RUN chmod +x ${APP_DIR}/workbench/setup-env.sh 4 | RUN ${APP_DIR}/workbench/setup-env.sh -------------------------------------------------------------------------------- /box.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"FluentAPI", 3 | "version":"1.0.0", 4 | "author":"Ortus Solutions", 5 | "slug":"fluentapi", 6 | "type":"mvc", 7 | "homepage":"https://github.com/lmajano/modern-functional-fluent-cfml-rest", 8 | "documentation":"https://github.com/lmajano/modern-functional-fluent-cfml-rest", 9 | "repository":{ 10 | "type":"git", 11 | "url":"https://github.com/lmajano/modern-functional-fluent-cfml-rest" 12 | }, 13 | "bugs":"https://github.com/lmajano/modern-functional-fluent-cfml-rest/issues", 14 | "shortDescription":"Demo repo for functional APIs", 15 | "contributors":[ 16 | "Luis Majano ", 17 | "Gavin Pickin " 18 | ], 19 | "ignore":[], 20 | "devDependencies":{ 21 | "relax":"*", 22 | "route-visualizer":"*", 23 | "testbox":"be", 24 | "commandbox-dotenv":"*", 25 | "commandbox-docbox":"*", 26 | "commandbox-cfconfig":"*", 27 | "commandbox-cfformat":"*", 28 | "commandbox-migrations":"*" 29 | }, 30 | "dependencies":{ 31 | "coldbox":"be", 32 | "cbswagger":"^3", 33 | "cbvalidation":"^4", 34 | "cors":"^3", 35 | "mementifier":"^3", 36 | "cbdebugger":"^4", 37 | "cbSwaggerUI":"^1.2.1" 38 | }, 39 | "installPaths":{ 40 | "coldbox":"coldbox/", 41 | "relax":"modules/relax/", 42 | "testbox":"testbox/", 43 | "cbSwagger":"modules/cbSwagger/", 44 | "cbvalidation":"modules/cbvalidation/", 45 | "route-visualizer":"modules/route-visualizer/", 46 | "cors":"modules/cors/", 47 | "mementifier":"modules/mementifier/", 48 | "cbdebugger":"modules/cbdebugger/", 49 | "cbSwaggerUI":"modules/cbSwaggerUI/" 50 | }, 51 | "testbox":{ 52 | "runner":"http://localhost:60146/tests/runner.cfm" 53 | }, 54 | "scripts":{ 55 | "start:adobe":"server start serverConfigFile=server-adobe.json --force", 56 | "start:boxlang":"server start serverConfigFile=server-boxlang.json --force --debug", 57 | "lint":"cflint **.cf* --text --html --json --!exitOnError --suppress", 58 | "format":"cfformat run config,Application.cfc,modules_app/**/*.cfc,tests/specs/**/*.cfc --overwrite --verbose", 59 | "format:watch":"cfformat watch config,Application.cfc,modules_app/**/*.cfc,tests/specs/**/*.cfc ./.cfformat.json", 60 | "test:v1":"testbox run directory=tests/specs/integration/api-v1", 61 | "test:v2":"testbox run directory=tests/specs/integration/api-v2", 62 | "test:v3":"testbox run directory=tests/specs/integration/api-v3", 63 | "test:v4":"testbox run directory=tests/specs/integration/api-v4", 64 | "test:v5":"testbox run directory=tests/specs/integration/api-v5", 65 | "test:v6":"testbox run directory=tests/specs/integration/api-v6" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /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 | 7 | abort; 8 | 9 | } 10 | -------------------------------------------------------------------------------- /config/CacheBox.cfc: -------------------------------------------------------------------------------- 1 | component { 2 | 3 | /** 4 | * Configure CacheBox for ColdBox Application Operation 5 | */ 6 | function configure(){ 7 | /** 8 | * -------------------------------------------------------------------------- 9 | * CacheBox Configuration (https://cachebox.ortusbooks.com) 10 | * -------------------------------------------------------------------------- 11 | */ 12 | cacheBox = { 13 | /** 14 | * -------------------------------------------------------------------------- 15 | * Default Cache Configuration 16 | * -------------------------------------------------------------------------- 17 | * The defaultCache has an implicit name "default" which is a reserved cache name 18 | * It also has a default provider of cachebox which cannot be changed. 19 | * All timeouts are in minutes 20 | */ 21 | defaultCache : { 22 | objectDefaultTimeout : 120, // two hours default 23 | objectDefaultLastAccessTimeout : 30, // 30 minutes idle time 24 | useLastAccessTimeouts : true, 25 | reapFrequency : 5, 26 | freeMemoryPercentageThreshold : 0, 27 | evictionPolicy : "LRU", 28 | evictCount : 1, 29 | maxObjects : 300, 30 | objectStore : "ConcurrentStore", // guaranteed objects 31 | coldboxEnabled : true 32 | }, 33 | /** 34 | * -------------------------------------------------------------------------- 35 | * Custom Cache Regions 36 | * -------------------------------------------------------------------------- 37 | * You can use this section to register different cache regions and map them 38 | * to different cache providers 39 | */ 40 | caches : { 41 | /** 42 | * -------------------------------------------------------------------------- 43 | * ColdBox Template Cache 44 | * -------------------------------------------------------------------------- 45 | * The ColdBox Template cache region is used for event/view caching and 46 | * other internal facilities that might require a more elastic cache. 47 | */ 48 | template : { 49 | provider : "coldbox.system.cache.providers.CacheBoxColdBoxProvider", 50 | properties : { 51 | objectDefaultTimeout : 120, 52 | objectDefaultLastAccessTimeout : 30, 53 | useLastAccessTimeouts : true, 54 | freeMemoryPercentageThreshold : 0, 55 | reapFrequency : 5, 56 | evictionPolicy : "LRU", 57 | evictCount : 2, 58 | maxObjects : 300, 59 | objectStore : "ConcurrentSoftReferenceStore" // memory sensitive 60 | } 61 | } 62 | } 63 | }; 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /config/Coldbox.cfc: -------------------------------------------------------------------------------- 1 | component { 2 | 3 | /** 4 | * Configure the ColdBox App For Production 5 | * https://coldbox.ortusbooks.com/getting-started/configuration 6 | */ 7 | function configure(){ 8 | /** 9 | * -------------------------------------------------------------------------- 10 | * ColdBox Directives 11 | * -------------------------------------------------------------------------- 12 | * Here you can configure ColdBox for operation. Remember tha these directives below 13 | * are for PRODUCTION. If you want different settings for other environments make sure 14 | * you create the appropriate functions and define the environment in your .env or 15 | * in the `environments` struct. 16 | */ 17 | coldbox = { 18 | // Application Setup 19 | appName : getSystemSetting( "APPNAME", "Fluent API" ), 20 | eventName : "event", 21 | // Development Settings 22 | reinitPassword : getSystemSetting( "COLDBOX_REINITPASSWORD", "" ), 23 | reinitKey : "fwreinit", 24 | handlersIndexAutoReload : true, 25 | // Implicit Events 26 | defaultEvent : "v1:Echo.index", 27 | requestStartHandler : "", 28 | requestEndHandler : "", 29 | applicationStartHandler : "", 30 | applicationEndHandler : "", 31 | sessionStartHandler : "", 32 | sessionEndHandler : "", 33 | missingTemplateHandler : "", 34 | // Extension Points 35 | applicationHelper : "", 36 | viewsHelper : "", 37 | modulesExternalLocation : [], 38 | viewsExternalLocation : "", 39 | layoutsExternalLocation : "", 40 | handlersExternalLocation : "", 41 | requestContextDecorator : "", 42 | controllerDecorator : "", 43 | // Error/Exception Handling 44 | invalidHTTPMethodHandler : "", 45 | exceptionHandler : "v1:Echo.onError", 46 | invalidEventHandler : "v1:Echo.onInvalidRoute", 47 | customErrorTemplate : "", 48 | // Application Aspects 49 | handlerCaching : true, 50 | eventCaching : true, 51 | viewCaching : false, 52 | // Will automatically do a mapDirectory() on your `models` for you. 53 | autoMapModels : true, 54 | // Auto converts a json body payload into the RC 55 | jsonPayloadToRC : true 56 | }; 57 | 58 | /** 59 | * -------------------------------------------------------------------------- 60 | * Custom Settings 61 | * -------------------------------------------------------------------------- 62 | */ 63 | settings = {}; 64 | 65 | /** 66 | * -------------------------------------------------------------------------- 67 | * Environment Detection 68 | * -------------------------------------------------------------------------- 69 | * By default we look in your `.env` file for an `environment` key, if not, 70 | * then we look into this structure or if you have a function called `detectEnvironment()` 71 | * If you use this setting, then each key is the name of the environment and the value is 72 | * the regex patterns to match against cgi.http_host. 73 | * 74 | * Uncomment to use, but make sure your .env ENVIRONMENT key is also removed. 75 | */ 76 | // environments = { development : "localhost,^127\.0\.0\.1" }; 77 | 78 | /** 79 | * -------------------------------------------------------------------------- 80 | * Application Logging (https://logbox.ortusbooks.com) 81 | * -------------------------------------------------------------------------- 82 | * By Default we log to the console, but you can add many appenders or destinations to log to. 83 | * You can also choose the logging level of the root logger, or even the actual appender. 84 | */ 85 | logBox = { 86 | // Define Appenders 87 | appenders : { console : { class : "coldbox.system.logging.appenders.ConsoleAppender" } }, 88 | // Root Logger 89 | root : { levelmax : "INFO", appenders : "*" }, 90 | // Implicit Level Categories 91 | info : [ "coldbox.system" ] 92 | }; 93 | 94 | /** 95 | * -------------------------------------------------------------------------- 96 | * Layout Settings 97 | * -------------------------------------------------------------------------- 98 | */ 99 | layoutSettings = { defaultLayout : "", defaultView : "" }; 100 | 101 | /** 102 | * -------------------------------------------------------------------------- 103 | * Custom Interception Points 104 | * -------------------------------------------------------------------------- 105 | */ 106 | interceptorSettings = { customInterceptionPoints : [] }; 107 | 108 | /** 109 | * -------------------------------------------------------------------------- 110 | * Application Interceptors 111 | * -------------------------------------------------------------------------- 112 | * Remember that the order of declaration is the order they will be registered and fired 113 | */ 114 | interceptors = []; 115 | 116 | /** 117 | * -------------------------------------------------------------------------- 118 | * Module Settings 119 | * -------------------------------------------------------------------------- 120 | * Each module has it's own configuration structures, so make sure you follow 121 | * the module's instructions on settings. 122 | * 123 | * Each key is the name of the module: 124 | * 125 | * myModule = { 126 | * 127 | * } 128 | */ 129 | moduleSettings = {}; 130 | } 131 | 132 | /** 133 | * Development environment 134 | */ 135 | function development(){ 136 | coldbox.handlersIndexAutoReload = true; 137 | coldbox.handlerCaching = false; 138 | coldbox.reinitpassword = ""; 139 | coldbox.customErrorTemplate = "/coldbox/system/exceptions/Whoops.cfm"; 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /config/Router.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * This is your application router. From here you can controll all the incoming routes to your application. 3 | * 4 | * https://coldbox.ortusbooks.com/the-basics/routing 5 | */ 6 | component { 7 | 8 | function configure(){ 9 | /** 10 | * -------------------------------------------------------------------------- 11 | * Router Configuration Directives 12 | * -------------------------------------------------------------------------- 13 | * https://coldbox.ortusbooks.com/the-basics/routing/application-router#configuration-methods 14 | */ 15 | setFullRewrites( true ); 16 | 17 | /** 18 | * -------------------------------------------------------------------------- 19 | * App Routes 20 | * -------------------------------------------------------------------------- 21 | * Here is where you can register the routes for your web application! 22 | * Go get Funky! 23 | */ 24 | 25 | // A nice healthcheck route example 26 | route( "/healthcheck", function( event, rc, prc ){ 27 | return "Ok!"; 28 | } ); 29 | 30 | // @app_routes@ 31 | 32 | // Conventions-Based Routing 33 | route( ":handler/:action?" ).end(); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /config/Scheduler.cfc: -------------------------------------------------------------------------------- 1 | component { 2 | 3 | /** 4 | * Configure the ColdBox Scheduler 5 | * https://coldbox.ortusbooks.com/digging-deeper/scheduled-tasks 6 | */ 7 | function configure(){ 8 | /** 9 | * -------------------------------------------------------------------------- 10 | * Configuration Methods 11 | * -------------------------------------------------------------------------- 12 | * From here you can set global configurations for the scheduler 13 | * - setTimezone( ) : change the timezone for ALL tasks 14 | * - setExecutor( executorObject ) : change the executor if needed 15 | */ 16 | 17 | 18 | /** 19 | * -------------------------------------------------------------------------- 20 | * Register Scheduled Tasks 21 | * -------------------------------------------------------------------------- 22 | * You register tasks with the task() method and get back a ColdBoxScheduledTask object 23 | * that you can use to register your tasks configurations. 24 | */ 25 | } 26 | 27 | /** 28 | * Called before the scheduler is going to be shutdown 29 | */ 30 | function onShutdown(){ 31 | } 32 | 33 | /** 34 | * Called after the scheduler has registered all schedules 35 | */ 36 | function onStartup(){ 37 | } 38 | 39 | /** 40 | * Called whenever ANY task fails 41 | * 42 | * @task The task that got executed 43 | * @exception The ColdFusion exception object 44 | */ 45 | function onAnyTaskError( required task, required exception ){ 46 | } 47 | 48 | /** 49 | * Called whenever ANY task succeeds 50 | * 51 | * @task The task that got executed 52 | * @result The result (if any) that the task produced 53 | */ 54 | function onAnyTaskSuccess( required task, result ){ 55 | } 56 | 57 | /** 58 | * Called before ANY task runs 59 | * 60 | * @task The task about to be executed 61 | */ 62 | function beforeAnyTask( required task ){ 63 | } 64 | 65 | /** 66 | * Called after ANY task runs 67 | * 68 | * @task The task that got executed 69 | * @result The result (if any) that the task produced 70 | */ 71 | function afterAnyTask( required task, result ){ 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /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 | * -------------------------------------------------------------------------- 9 | * WireBox Configuration (https://wirebox.ortusbooks.com) 10 | * -------------------------------------------------------------------------- 11 | * Configure WireBox 12 | */ 13 | wireBox = { 14 | // Scope registration, automatically register a wirebox injector instance on any CF scope 15 | // By default it registeres itself on application scope 16 | scopeRegistration : { 17 | enabled : true, 18 | scope : "application", // server, cluster, session, application 19 | key : "wireBox" 20 | }, 21 | // DSL Namespace registrations 22 | customDSL : {}, 23 | // Custom Storage Scopes 24 | customScopes : {}, 25 | // Package scan locations 26 | scanLocations : [], 27 | // Stop Recursions 28 | stopRecursions : [], 29 | // Parent Injector to assign to the configured injector, this must be an object reference 30 | parentInjector : "", 31 | // Register all event listeners here, they are created in the specified order 32 | listeners : [] 33 | }; 34 | 35 | // Map Bindings below 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /config/modules/cbdebugger.cfc: -------------------------------------------------------------------------------- 1 | component { 2 | 3 | function configure(){ 4 | return { 5 | // This flag enables/disables the tracking of request data to our storage facilities 6 | // To disable all tracking, turn this master key off 7 | enabled : true, 8 | // This setting controls if you will activate the debugger for visualizations ONLY 9 | // The debugger will still track requests even in non debug mode. 10 | debugMode : controller.getSetting( name = "environment", defaultValue = "production" ) == "development", 11 | // The URL password to use to activate it on demand 12 | debugPassword : "cb:null", 13 | // This flag enables/disables the end of request debugger panel docked to the bottem of the page. 14 | // If you disable i, then the only way to visualize the debugger is via the `/cbdebugger` endpoint 15 | requestPanelDock : true, 16 | // Request Tracker Options 17 | requestTracker : { 18 | // Store the request profilers in heap memory or in cachebox, default is memory 19 | storage : "memory", 20 | // Which cache region to store the profilers in 21 | cacheName : "template", 22 | // Track all cbdebugger events, by default this is off, turn on, when actually profiling yourself :) How Meta! 23 | trackDebuggerEvents : false, 24 | // Expand by default the tracker panel or not 25 | expanded : false, 26 | // Slow request threshold in milliseconds, if execution time is above it, we mark those transactions as red 27 | slowExecutionThreshold : 1000, 28 | // How many tracking profilers to keep in stack 29 | maxProfilers : 50, 30 | // If enabled, the debugger will monitor the creation time of CFC objects via WireBox 31 | profileWireBoxObjectCreation : false, 32 | // Profile model objects annotated with the `profile` annotation 33 | profileObjects : false, 34 | // If enabled, will trace the results of any methods that are being profiled 35 | traceObjectResults : false, 36 | // Profile Custom or Core interception points 37 | profileInterceptions : false, 38 | // By default all interception events are excluded, you must include what you want to profile 39 | includedInterceptions : [], 40 | // Control the execution timers 41 | executionTimers : { 42 | expanded : true, 43 | // Slow transaction timers in milliseconds, if execution time of the timer is above it, we mark it 44 | slowTimerThreshold : 250 45 | }, 46 | // Control the coldbox info reporting 47 | coldboxInfo : { expanded : false }, 48 | // Control the http request reporting 49 | httpRequest : { 50 | expanded : false, 51 | // If enabled, we will profile HTTP Body content, disabled by default as it contains lots of data 52 | profileHTTPBody : false 53 | } 54 | }, 55 | // ColdBox Tracer Appender Messages 56 | tracers : { enabled : true, expanded : false }, 57 | // Request Collections Reporting 58 | collections : { 59 | // Enable tracking 60 | enabled : false, 61 | // Expanded panel or not 62 | expanded : false, 63 | // How many rows to dump for object collections 64 | maxQueryRows : 50, 65 | // How many levels to output on dumps for objects 66 | maxDumpTop : 5 67 | }, 68 | // CacheBox Reporting 69 | cachebox : { enabled : true, expanded : false }, 70 | // Modules Reporting 71 | modules : { enabled : true, expanded : false }, 72 | // Quick and QB Reporting 73 | qb : { 74 | enabled : false, 75 | expanded : false, 76 | // Log the binding parameters 77 | logParams : true 78 | }, 79 | // cborm Reporting 80 | cborm : { 81 | enabled : false, 82 | expanded : false, 83 | // Log the binding parameters 84 | logParams : false 85 | }, 86 | // Adobe ColdFusion SQL Collector 87 | acfSql : { enabled : true, expanded : false, logParams : true }, 88 | // Lucee SQL Collector 89 | luceeSQL : { enabled : false, expanded : false, logParams : true }, 90 | async : { enabled : true, expanded : false } 91 | }; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /config/modules/cbswagger.cfc: -------------------------------------------------------------------------------- 1 | component { 2 | 3 | function configure(){ 4 | return { 5 | // The route prefix to search. Routes beginning with this prefix will be determined to be api routes 6 | "routes" : [ "api/v6" ], 7 | // Any routes to exclude 8 | "excludeRoutes" : [], 9 | // The default output format: json or yml 10 | "defaultFormat" : "json", 11 | // 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 ) 12 | "samplesPath" : "resources/apidocs", 13 | // Information about your API 14 | "info" : { 15 | // A title for your API 16 | "title" : "Fluent API for SoapBox Twitter clone", 17 | // A description of your API 18 | "description" : "This Fluent API provides data the for SoapBox Twitter clone", 19 | // A terms of service URL for your API 20 | "termsOfService" : "", 21 | // The contact email address 22 | "contact" : { 23 | "name" : "Gavin Pickin", 24 | "url" : "https://www.ortussolutions.com", 25 | "email" : "gavin@ortussolutions.com" 26 | }, 27 | // A url to the License of your API 28 | "license" : { 29 | "name" : "Apache 2.0", 30 | "url" : "https://www.apache.org/licenses/LICENSE-2.0.html" 31 | }, 32 | // The version of your API 33 | "version" : "1.0.0" 34 | }, 35 | // Tags 36 | "tags" : [], 37 | // https://swagger.io/specification/#externalDocumentationObject 38 | "externalDocs" : { 39 | "description" : "Find more info here", 40 | "url" : "https://blog.readme.io/an-example-filled-guide-to-swagger-3-2/" 41 | }, 42 | // https://swagger.io/specification/#serverObject 43 | "servers" : [ 44 | { 45 | "url" : "http://127.0.0.1:60146", 46 | "description" : "Local development" 47 | } 48 | ], 49 | // An element to hold various schemas for the specification. 50 | // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#componentsObject 51 | "components" : { 52 | // Define your security schemes here 53 | // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#securitySchemeObject 54 | "securitySchemes" : { 55 | // "ApiKeyAuth" : { 56 | // "type" : "apiKey", 57 | // "description" : "User your JWT as an Api Key for security", 58 | // "name" : "x-api-key", 59 | // "in" : "header" 60 | // }, 61 | // "bearerAuth" : { 62 | // "type" : "http", 63 | // "scheme" : "bearer", 64 | // "bearerFormat" : "JWT" 65 | // } 66 | } 67 | } 68 | 69 | // A default declaration of Security Requirement Objects to be used across the API. 70 | // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#securityRequirementObject 71 | // Only one of these requirements needs to be satisfied to authorize a request. 72 | // Individual operations may set their own requirements with `@security` 73 | // "security" : [ 74 | // { "APIKey" : [] }, 75 | // { "UserSecurity" : [] } 76 | // ] 77 | }; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /config/modules/mementifier.cfc: -------------------------------------------------------------------------------- 1 | component{ 2 | 3 | /** 4 | * Mementifier settings: https://forgebox.io/view/mementifier 5 | */ 6 | function configure(){ 7 | return { 8 | // Turn on to use the ISO8601 date/time formatting on all processed date/time properites, else use the masks 9 | iso8601Format : true, 10 | // The default date mask to use for date properties 11 | dateMask : "yyyy-MM-dd", 12 | // The default time mask to use for date properties 13 | timeMask : "HH:mm: ss", 14 | // Enable orm auto default includes: If true and an object doesn't have any `memento` struct defined 15 | // this module will create it with all properties and relationships it can find for the target entity 16 | // leveraging the cborm module. 17 | ormAutoIncludes : true, 18 | // The default value for relationships/getters which return null 19 | nullDefaultValue : "", 20 | // Don't check for getters before invoking them 21 | trustedGetters : false, 22 | // If not empty, convert all date/times to the specific timezone 23 | convertToTimezone : "UTC" 24 | }; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /index.cfm: -------------------------------------------------------------------------------- 1 |  8 | -------------------------------------------------------------------------------- /modules_app/api/ModuleConfig.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * API Core Module Configuration 3 | */ 4 | component { 5 | 6 | // Module Properties 7 | this.title = "api"; 8 | this.description = "Base API Module"; 9 | this.version = "1.0.0"; 10 | 11 | // Module Entry Point in the URI: http://yourapp/api 12 | this.entryPoint = "api"; 13 | // Inheritable entry point. 14 | this.inheritEntryPoint = true; 15 | // Model Namespace 16 | this.modelNamespace = "api"; 17 | // CF Mapping 18 | this.cfmapping = "api"; 19 | // Module Dependencies 20 | this.dependencies = []; 21 | 22 | /** 23 | * Configure the module 24 | */ 25 | function configure(){ 26 | } 27 | 28 | /** 29 | * Fired when the module is registered and activated. 30 | */ 31 | function onLoad(){ 32 | } 33 | 34 | /** 35 | * Fired when the module is unregistered and unloaded 36 | */ 37 | function onUnload(){ 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /modules_app/api/config/Router.cfc: -------------------------------------------------------------------------------- 1 | component { 2 | 3 | function configure(){ 4 | } 5 | 6 | } 7 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v1/ModuleConfig.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * v1 Module Config 3 | */ 4 | component { 5 | 6 | // Module Properties 7 | this.title = "v1"; 8 | // Module Entry Point 9 | this.entryPoint = "v1"; 10 | // Inherit URI entry point from parent, so this will be /api/v1 11 | this.inheritEntryPoint = true; 12 | // Model Namespace 13 | this.modelNamespace = "v1"; 14 | // CF Mapping 15 | this.cfmapping = "v1"; 16 | // Module Dependencies 17 | this.dependencies = []; 18 | 19 | function configure(){ 20 | } 21 | 22 | /** 23 | * Fired when the module is registered and activated. 24 | */ 25 | function onLoad(){ 26 | } 27 | 28 | /** 29 | * Fired when the module is unregistered and unloaded 30 | */ 31 | function onUnload(){ 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v1/config/Router.cfc: -------------------------------------------------------------------------------- 1 | component { 2 | 3 | function configure(){ 4 | // Version Entry Point 5 | route( "/", "echo.index" ); 6 | // No more routing as we are using convention based routing thanks to ColdBox 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /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="coldbox.system.RestHandler" { 5 | 6 | // REST Allowed HTTP Methods Ex: this.allowedMethods = {delete='POST,DELETE',index='GET'} 7 | this.allowedMethods = {}; 8 | 9 | /** 10 | * Say Hello v1 11 | */ 12 | function index( event, rc, prc ){ 13 | event.getResponse().setData( "Welcome to my ColdBox RESTFul Service v1" ); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v1/handlers/Rants.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * My RESTFul Rants Event Handler which inherits from the module `api` 3 | * Since we inherit from the RestHandler we get lots of goodies like automatic HTTP method protection, 4 | * missing routes, invalid routes, and much more. 5 | * 6 | * @see https://coldbox.ortusbooks.com/digging-deeper/rest-handler 7 | * @see https://coldbox.ortusbooks.com/digging-deeper/rest-handler#rest-handler-security 8 | */ 9 | component extends="coldbox.system.RestHandler" { 10 | 11 | // DI 12 | property name="rantService" inject="RantService@v1"; 13 | property name="userService" inject="UserService@v1"; 14 | 15 | /** 16 | * Returns a list of Rants 17 | */ 18 | any function list( event, rc, prc ){ 19 | var rants = rantService.list(); 20 | prc.response.setData( rants ); 21 | } 22 | 23 | /** 24 | * Returns a single Rant 25 | */ 26 | function view( event, rc, prc ){ 27 | if ( !structKeyExists( rc, "rantId" ) ) { 28 | prc.response.setError( true ); 29 | prc.response.setStatusCode( 412 ); 30 | prc.response.addMessage( "rantId is required" ); 31 | return; 32 | } 33 | if ( !isValid( "uuid", rc.rantId ) ) { 34 | prc.response.setError( true ); 35 | prc.response.setStatusCode( 412 ); 36 | prc.response.addMessage( "rantId must be a UUID" ); 37 | return; 38 | } 39 | var rant = rantService.getRant( rc.rantId ); 40 | 41 | if ( rant.recordcount ) { 42 | prc.response.setData( queryGetRow( rant, 1 ) ); 43 | } else { 44 | prc.response.setError( true ); 45 | prc.response.setStatusCode( 404 ); 46 | prc.response.addMessage( "Error loading Rant - Rant not found" ); 47 | } 48 | } 49 | 50 | /** 51 | * Deletes a single Rant 52 | */ 53 | function delete( event, rc, prc ){ 54 | if ( !structKeyExists( rc, "rantId" ) ) { 55 | prc.response.setError( true ); 56 | prc.response.setStatusCode( 412 ); 57 | prc.response.addMessage( "rantId is required" ); 58 | } else if ( !isValid( "UUID", rc.rantId ) ) { 59 | prc.response.setError( true ); 60 | prc.response.setStatusCode( 412 ); 61 | prc.response.addMessage( "rantId must be a UUID" ); 62 | } else { 63 | var result = rantService.delete( rc.rantId ); 64 | if ( result.recordcount > 0 ) { 65 | prc.response.addMessage( "Rant deleted" ); 66 | } else { 67 | prc.response.setError( true ); 68 | prc.response.setStatusCode( 404 ); 69 | prc.response.addMessage( "Rant not found" ); 70 | } 71 | } 72 | } 73 | 74 | /** 75 | * Creates a new Rant 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 ( !isValid( "uuid", rc.userId ) ) { 97 | prc.response.setError( true ); 98 | prc.response.setStatusCode( 412 ); 99 | prc.response.addMessage( "userId must be a UUID" ); 100 | return; 101 | } 102 | var user = userService.get( rc.userId ) 103 | if ( !user.recordcount ) { 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 | function save( event, rc, prc ){ 126 | if ( !structKeyExists( rc, "body" ) ) { 127 | prc.response.setError( true ); 128 | prc.response.setStatusCode( 412 ); 129 | prc.response.addMessage( "Rant body is required" ); 130 | return 131 | } 132 | if ( !rc.body.trim().len() ) { 133 | prc.response.setError( true ); 134 | prc.response.setStatusCode( 412 ); 135 | prc.response.addMessage( "Rant body cannot be empty" ); 136 | return 137 | } 138 | if ( !structKeyExists( rc, "rantId" ) ) { 139 | prc.response.setError( true ); 140 | prc.response.setStatusCode( 412 ); 141 | prc.response.addMessage( "rantId is required" ); 142 | return 143 | } 144 | if ( !isValid( "uuid", rc.rantId ) ) { 145 | prc.response.setError( true ); 146 | prc.response.setStatusCode( 412 ); 147 | prc.response.addMessage( "rantId must be a UUID" ); 148 | return 149 | } 150 | var rant = rantService.getRant( rc.rantId ) 151 | if ( !rant.recordcount ) { 152 | prc.response.setError( true ); 153 | prc.response.setStatusCode( 404 ); 154 | prc.response.addMessage( "Rant not found" ); 155 | return; 156 | } 157 | if ( !structKeyExists( rc, "userId" ) ) { 158 | prc.response.setError( true ); 159 | prc.response.setStatusCode( 412 ); 160 | prc.response.addMessage( "userId is required" ); 161 | return; 162 | } 163 | if ( !isValid( "UUID", rc.userId ) ) { 164 | prc.response.setError( true ); 165 | prc.response.setStatusCode( 412 ); 166 | prc.response.addMessage( "userId must be a UUID" ); 167 | return; 168 | } 169 | var user = userService.get( rc.userId ) 170 | if ( !user.recordcount ) { 171 | prc.response.setError( true ); 172 | prc.response.setStatusCode( 404 ); 173 | prc.response.addMessage( "User not found" ); 174 | return; 175 | } 176 | var result = rantService.update( 177 | body = rc.body, 178 | userId = rc.userId, 179 | rantId = rc.rantId 180 | ); 181 | if ( result.recordcount ) { 182 | prc.response.addMessage( "Rant Updated" ); 183 | } else { 184 | prc.response.setError( true ); 185 | prc.response.setStatusCode( 500 ); 186 | prc.response.addMessage( "Error updating Rant" ); 187 | return; 188 | } 189 | } 190 | 191 | } 192 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v1/models/RantService.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * I am the Rant Service V1 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", {} ); 15 | } 16 | 17 | function getRant( required rantId ){ 18 | return queryExecute( 19 | "select * from rants 20 | where id = :rantId", 21 | { rantId : arguments.rantId } 22 | ); 23 | } 24 | 25 | function delete( required rantId ){ 26 | queryExecute( 27 | "delete from rants 28 | where id = :rantId", 29 | { rantId : arguments.rantId }, 30 | { result : "local.result" } 31 | ); 32 | return local.result; 33 | } 34 | 35 | function create( required body, required userId ){ 36 | var now = now(); 37 | var newId = createUUID(); 38 | queryExecute( 39 | "insert into rants 40 | set 41 | id = :rantId, 42 | body = :body, 43 | userId = :userId 44 | ", 45 | { 46 | rantId : newId, 47 | body : { value : "#body#", cfsqltype : "varchar" }, 48 | userId : arguments.userId 49 | }, 50 | { result : "local.result" } 51 | ); 52 | local.result.generatedKey = newId; 53 | return local.result; 54 | } 55 | 56 | function update( required body, required rantId ){ 57 | var now = now(); 58 | queryExecute( 59 | "update rants 60 | set 61 | body = :body, 62 | updatedDate = :updatedDate 63 | where id = :rantId 64 | ", 65 | { 66 | rantId : arguments.rantId, 67 | body : { value : "#body#", cfsqltype : "varchar" }, 68 | updatedDate : { value : "#now#", cfsqltype : "timestamp" } 69 | }, 70 | { result : "local.result" } 71 | ); 72 | return local.result; 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v1/models/UserService.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * I am the User Service V1 3 | */ 4 | component singleton accessors="true" { 5 | 6 | /** 7 | * Constructor 8 | */ 9 | UserService function init(){ 10 | return this; 11 | } 12 | 13 | function get( required string userId ){ 14 | return queryExecute( 15 | "select * from users 16 | where id = :userId", 17 | { userId : arguments.userId } 18 | ); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v2/ModuleConfig.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * v2 Module Config 3 | */ 4 | component { 5 | 6 | // Module Properties 7 | this.title = "v2"; 8 | // Module Entry Point 9 | this.entryPoint = "v2"; 10 | // Inherit entry point from parent, so this will be /api/v1 11 | this.inheritEntryPoint = true; 12 | // Model Namespace 13 | this.modelNamespace = "v2"; 14 | // CF Mapping 15 | this.cfmapping = "v2"; 16 | // Module Dependencies 17 | this.dependencies = []; 18 | 19 | function configure(){ 20 | } 21 | 22 | /** 23 | * Fired when the module is registered and activated. 24 | */ 25 | function onLoad(){ 26 | } 27 | 28 | /** 29 | * Fired when the module is unregistered and unloaded 30 | */ 31 | function onUnload(){ 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v2/config/Router.cfc: -------------------------------------------------------------------------------- 1 | component { 2 | 3 | function configure(){ 4 | // CRUD 5 | post( "/rants/create", "rants.create" ) 6 | delete( "/rants/:rantId/delete", "rants.delete" ) 7 | put( "/rants/:rantId/save", "rants.save" ) 8 | get( "/rants/:rantId", "rants.view" ) 9 | get( "/rants", "rants.list" ) 10 | 11 | // Entry Point 12 | route( "/", "echo.index" ); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /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="coldbox.system.RestHandler" { 5 | 6 | // REST Allowed HTTP Methods Ex: this.allowedMethods = {delete='POST,DELETE',index='GET'} 7 | this.allowedMethods = {}; 8 | 9 | /** 10 | * Index 11 | */ 12 | any function index( event, rc, prc ){ 13 | prc.response.setData( "Welcome to my ColdBox RESTFul Service V2" ); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v2/handlers/Rants.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * My RESTFul Rants Event Handler which inherits from the module `api` 3 | * Since we inherit from the RestHandler we get lots of goodies like automatic HTTP method protection, 4 | * missing routes, invalid routes, and much more. 5 | * 6 | * @see https://coldbox.ortusbooks.com/digging-deeper/rest-handler 7 | * @see https://coldbox.ortusbooks.com/digging-deeper/rest-handler#rest-handler-security 8 | */ 9 | component extends="coldbox.system.RestHandler" { 10 | 11 | // DI 12 | property name="rantService" inject="RantService@v2"; 13 | property name="userService" inject="UserService@v2"; 14 | 15 | /** 16 | * Returns a list of Rants 17 | */ 18 | any function list( event, rc, prc ){ 19 | prc.response.setData( rantService.list() ); 20 | } 21 | 22 | /** 23 | * Returns a single Rant 24 | */ 25 | function view( event, rc, prc ){ 26 | var validationResults = validate( 27 | target = rc, 28 | constraints = { rantId : { required : true, type : "uuid" } } 29 | ); 30 | if ( validationResults.hasErrors() ) { 31 | prc.response.setErrorMessage( validationResults.getAllErrors(), 412 ); 32 | return; 33 | } 34 | var rant = rantService.get( rc.rantId ); 35 | 36 | if ( !rant.isEmpty() ) { 37 | prc.response.setData( rant ) 38 | } else { 39 | prc.response.setErrorMessage( "Error loading Rant - Rant not found", 404 ); 40 | } 41 | } 42 | 43 | /** 44 | * Deletes a single Rant 45 | */ 46 | function delete( event, rc, prc ){ 47 | var validationResults = validate( 48 | target = rc, 49 | constraints = { rantId : { required : true, type : "uuid" } } 50 | ); 51 | if ( validationResults.hasErrors() ) { 52 | prc.response.setErrorMessage( validationResults.getAllErrors(), 412 ); 53 | return; 54 | } 55 | 56 | var result = rantService.delete( rc.rantId ); 57 | if ( result.recordcount > 0 ) { 58 | prc.response.addMessage( "Rant deleted" ); 59 | } else { 60 | prc.response.setErrorMessage( "Rant not found", 404 ); 61 | } 62 | } 63 | 64 | /** 65 | * Creates a new Rant 66 | */ 67 | function create( event, rc, prc ){ 68 | var validationResults = validate( 69 | target = rc, 70 | constraints = { 71 | userId : { required : true, type : "uuid" }, 72 | body : { required : true } 73 | } 74 | ); 75 | if ( validationResults.hasErrors() ) { 76 | prc.response.setErrorMessage( validationResults.getAllErrors(), 412 ); 77 | return; 78 | } 79 | if ( !userService.exists( rc.userId ) ) { 80 | prc.response.setErrorMessage( "User not found", 404 ); 81 | return; 82 | } 83 | var result = rantService.create( body = rc.body, userId = rc.userId ); 84 | if ( result.recordcount ) { 85 | prc.response.setData( { "rantId" : result.generatedKey } ); 86 | prc.response.addMessage( "Rant created" ); 87 | return; 88 | } else { 89 | prc.response.setErrorMessage( "Error creating Rant", 500 ); 90 | return; 91 | } 92 | } 93 | 94 | /** 95 | * Updates an Existing Rant 96 | */ 97 | function save( event, rc, prc ){ 98 | var validationResults = validate( 99 | target = rc, 100 | constraints = { 101 | rantId : { required : true, type : "uuid" }, 102 | body : { required : true }, 103 | userId : { required : true, type : "uuid" } 104 | } 105 | ); 106 | if ( validationResults.hasErrors() ) { 107 | prc.response.setErrorMessage( validationResults.getAllErrors(), 412 ); 108 | return; 109 | } 110 | 111 | if ( !rantService.exists( rc.rantId ) ) { 112 | prc.response.setErrorMessage( "Rant not found", 404 ); 113 | return; 114 | } 115 | if ( !userService.exists( rc.userId ) ) { 116 | prc.response.setErrorMessage( "User not found", 404 ); 117 | return; 118 | } 119 | var result = rantService.update( 120 | body = rc.body, 121 | userId = rc.userId, 122 | rantId = rc.rantId 123 | ); 124 | if ( result.recordcount ) { 125 | prc.response.addMessage( "Rant Updated" ); 126 | } else { 127 | prc.response.setErrorMessage( "Error updating Rant", 500 ); 128 | return; 129 | } 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /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 | array function list(){ 14 | return queryExecute( 15 | "select * from rants ORDER BY createdDate DESC", 16 | {}, 17 | { returnType : "array" } 18 | ); 19 | } 20 | 21 | struct function get( required rantId ){ 22 | return queryExecute( 23 | "select * from rants 24 | where id = :rantId 25 | ", 26 | { rantId : arguments.rantId } 27 | ).reduce( ( result, row ) => { 28 | return row; 29 | }, {} ); 30 | } 31 | 32 | function delete( required rantId ){ 33 | queryExecute( 34 | "delete from rants 35 | where id = :rantId", 36 | { rantId : arguments.rantId }, 37 | { result : "local.result" } 38 | ); 39 | return local.result; 40 | } 41 | 42 | function create( required body, required userId ){ 43 | var now = now(); 44 | var newKey = createUUID(); 45 | queryExecute( 46 | "insert into rants 47 | set 48 | id = :rantId, 49 | body = :body, 50 | userId = :userId 51 | ", 52 | { 53 | rantId : newKey, 54 | body : { value : "#body#", cfsqltype : "longvarchar" }, 55 | userId : arguments.userId 56 | }, 57 | { result : "local.result" } 58 | ); 59 | local.result.generatedKey = newKey; 60 | return local.result; 61 | } 62 | 63 | function update( required body, required rantId ){ 64 | var now = now(); 65 | queryExecute( 66 | "update rants 67 | set 68 | body = :body, 69 | updatedDate = :updatedDate 70 | where id = :rantId 71 | ", 72 | { 73 | rantId : arguments.rantId, 74 | body : { value : "#body#", cfsqltype : "longvarchar" }, 75 | updatedDate : { value : "#now#", cfsqltype : "timestamp" } 76 | }, 77 | { result : "local.result" } 78 | ); 79 | return local.result; 80 | } 81 | 82 | boolean function exists( required rantId ){ 83 | return booleanFormat( 84 | queryExecute( 85 | "select id from rants 86 | where id = :rantId", 87 | { rantId : arguments.rantId } 88 | ).len() 89 | ) 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /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 | struct function get( required string userId ){ 14 | return queryExecute( 15 | "select * from users 16 | where id = :userId", 17 | { userId : arguments.userId } 18 | ).reduce( ( result, row ) => row, {} ); 19 | } 20 | 21 | boolean function exists( required string userId ){ 22 | return booleanFormat( 23 | queryExecute( 24 | "select id from users 25 | where id = :userId", 26 | { userId : arguments.userId } 27 | ).len() 28 | ) 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v3/ModuleConfig.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * v3 Module Config 3 | */ 4 | component { 5 | 6 | // Module Properties 7 | this.title = "v3"; 8 | // Module Entry Point 9 | this.entryPoint = "v3"; 10 | // Inherit entry point from parent, so this will be /api/v1 11 | this.inheritEntryPoint = true; 12 | // Model Namespace 13 | this.modelNamespace = "v3"; 14 | // CF Mapping 15 | this.cfmapping = "v3"; 16 | // Module Dependencies 17 | this.dependencies = []; 18 | 19 | function configure(){ 20 | } 21 | 22 | /** 23 | * Fired when the module is registered and activated. 24 | */ 25 | function onLoad(){ 26 | } 27 | 28 | /** 29 | * Fired when the module is unregistered and unloaded 30 | */ 31 | function onUnload(){ 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v3/config/Router.cfc: -------------------------------------------------------------------------------- 1 | component { 2 | 3 | function configure(){ 4 | // API Based Resourceful Routes: 5 | // https://coldbox.ortusbooks.com/the-basics/routing/routing-dsl/resourceful-routes#api-resourceful-routes 6 | apiResources( resource = "rants", parameterName = "rantId" ); 7 | 8 | // Entry Point 9 | route( "/", "echo.index" ); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /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="coldbox.system.RestHandler" { 5 | 6 | // REST Allowed HTTP Methods Ex: this.allowedMethods = {delete='POST,DELETE',index='GET'} 7 | this.allowedMethods = {}; 8 | 9 | /** 10 | * Index 11 | */ 12 | any function index( event, rc, prc ){ 13 | prc.response.setData( "Welcome to my ColdBox RESTFul Service v3" ); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v3/handlers/Rants.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * My RESTFul Rants Event Handler which inherits from the module `api` 3 | * Since we inherit from the RestHandler we get lots of goodies like automatic HTTP method protection, 4 | * missing routes, invalid routes, and much more. 5 | * 6 | * @see https://coldbox.ortusbooks.com/digging-deeper/rest-handler 7 | * @see https://coldbox.ortusbooks.com/digging-deeper/rest-handler#rest-handler-security 8 | */ 9 | component extends="coldbox.system.RestHandler" { 10 | 11 | // DI 12 | property name="rantService" inject="RantService@v3"; 13 | property name="userService" inject="UserService@v3"; 14 | 15 | /** 16 | * Returns a list of Rants 17 | */ 18 | any function index( event, rc, prc ){ 19 | prc.response.setData( rantService.list() ); 20 | } 21 | 22 | /** 23 | * Returns a single Rant 24 | */ 25 | function show( event, rc, prc ){ 26 | validateOrFail( target = rc, constraints = { rantId : { required : true, type : "uuid" } } ); 27 | prc.response.setData( rantService.getOrFail( rc.rantId ) ); 28 | } 29 | 30 | /** 31 | * Deletes a single Rant 32 | */ 33 | function delete( event, rc, prc ){ 34 | validateOrFail( target = rc, constraints = { rantId : { required : true, type : "uuid" } } ); 35 | rantService.existsOrFail( rc.rantId ); 36 | rantService.delete( rc.rantId ); 37 | prc.response.addMessage( "Rant deleted" ); 38 | } 39 | 40 | /** 41 | * Creates a new Rant 42 | */ 43 | function create( event, rc, prc ){ 44 | validateOrFail( 45 | target = rc, 46 | constraints = { 47 | userId : { required : true, type : "uuid" }, 48 | body : { required : true } 49 | } 50 | ); 51 | userService.existsOrFail( rc.userId ); 52 | var result = rantService.create( body = rc.body, userId = rc.userId ); 53 | prc.response.setData( { "rantId" : result.generatedKey } ); 54 | prc.response.addMessage( "Rant created" ); 55 | } 56 | 57 | /** 58 | * Updates an Existing Rant 59 | */ 60 | function update( event, rc, prc ){ 61 | validateOrFail( 62 | target = rc, 63 | constraints = { 64 | rantId : { required : true, type : "uuid" }, 65 | body : { required : true }, 66 | userId : { required : true, type : "uuid" } 67 | } 68 | ); 69 | 70 | rantService.existsOrFail( rc.rantId ); 71 | userService.existsOrFail( rc.userId ); 72 | 73 | rantService.update( 74 | body = rc.body, 75 | userId = rc.userId, 76 | rantId = rc.rantId 77 | ); 78 | 79 | prc.response.addMessage( "Rant Updated" ); 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v3/models/BaseService.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * I am the Base Service v3 3 | */ 4 | component accessors="true" { 5 | 6 | // Properties 7 | property name="entityName"; 8 | property name="tableName"; 9 | property name="primaryKey"; 10 | property name="serviceName"; 11 | property name="moduleName"; 12 | 13 | function init( 14 | entityName, 15 | tableName, 16 | primaryKey = "id", 17 | serviceName = "#arguments.entityName#Service", 18 | moduleName = "" 19 | ){ 20 | setEntityName( arguments.entityName ); 21 | setTableName( arguments.tableName ); 22 | setPrimaryKey( arguments.primaryKey ); 23 | setServiceName( arguments.serviceName ); 24 | setModuleName( arguments.moduleName ); 25 | return this; 26 | } 27 | 28 | /** 29 | * Check to see if there is a row with a matching primary key in the database. 30 | * Much faster than a full entity query and object load 31 | * 32 | * @id The primary key id to verify 33 | * 34 | * @return Returns true if there is a row with the matching Primary Key, otherwise returns false 35 | */ 36 | boolean function exists( required id ){ 37 | return booleanFormat( 38 | queryExecute( 39 | "select #getPrimaryKey()# from #getTableName()# 40 | where #getPrimaryKey()# = :id", 41 | { id : arguments.id } 42 | ).len() 43 | ) 44 | } 45 | 46 | /** 47 | * 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 48 | * 49 | * @id The primary key id to verify 50 | * 51 | * @return Returns true if there is a row with the matching Primary Key 52 | * 53 | * @throws EntityNotFound if the entity is not found 54 | */ 55 | boolean function existsOrFail( required id ){ 56 | if ( exists( argumentCollection = arguments ) ) { 57 | return true; 58 | } 59 | throw( type = "EntityNotFound", message = "#getEntityName()# Not Found" ); 60 | } 61 | 62 | /** 63 | * Query and load an entity if possible, else throw an error 64 | * 65 | * @id The primary key id to retrieve 66 | * 67 | * @return Returns the Entity if there is a row with the matching Primary Key 68 | * 69 | * @throws EntityNotFound if the entity is not found 70 | */ 71 | function getOrFail( required id ){ 72 | var maybeEntity = this.get( arguments.id ); 73 | if ( maybeEntity.isEmpty() ) { 74 | throw( type = "EntityNotFound", message = "#getEntityName()# Not Found" ); 75 | } 76 | return maybeEntity; 77 | } 78 | 79 | /** 80 | * Try to get an entity from the requested id 81 | * 82 | * @id The primary key id to retrieve 83 | * 84 | * @return Returns the Entity if there is a row with the matching Primary Key or an empty struct if not found 85 | */ 86 | struct function get( required id ){ 87 | return queryExecute( 88 | "select * from #getTableName()# 89 | where #getPrimaryKey()# = :id 90 | ", 91 | { id : arguments.id } 92 | ).reduce( ( result, row ) => row, {} ); 93 | } 94 | 95 | /** 96 | * Base delete entity 97 | */ 98 | function delete( required id ){ 99 | queryExecute( 100 | "delete from #getTableName()# 101 | where #getPrimaryKey()# = :id 102 | ", 103 | { id : arguments.id }, 104 | { result : "local.result" } 105 | ); 106 | return local.result; 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v3/models/RantService.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * I am the Rant Service v3 3 | */ 4 | component 5 | extends="v3.models.BaseService" 6 | singleton 7 | accessors="true" 8 | { 9 | 10 | /** 11 | * Constructor 12 | */ 13 | RantService function init(){ 14 | super.init( 15 | entityName = "rant", 16 | tableName = "rants", 17 | moduleName = "v3" 18 | ) 19 | return this; 20 | } 21 | 22 | array function list(){ 23 | return queryExecute( 24 | "select * from rants ORDER BY createdDate DESC", 25 | {}, 26 | { returnType : "array" } 27 | ); 28 | } 29 | 30 | function create( required body, required userId ){ 31 | var now = now(); 32 | var newKey = createUUID(); 33 | queryExecute( 34 | "insert into rants 35 | set 36 | id = :rantId, 37 | body = :body, 38 | userId = :userId 39 | ", 40 | { 41 | rantId : newKey, 42 | body : { value : "#body#", cfsqltype : "varchar" }, 43 | userId : arguments.userId 44 | }, 45 | { result : "local.result" } 46 | ); 47 | local.result.generatedKey = newKey; 48 | return local.result; 49 | } 50 | 51 | function update( required body, required rantId ){ 52 | var now = now(); 53 | queryExecute( 54 | "update rants 55 | set 56 | body = :body, 57 | updatedDate = :updatedDate 58 | where id = :rantId 59 | ", 60 | { 61 | rantId : arguments.rantId, 62 | body : { value : "#body#", cfsqltype : "varchar" }, 63 | updatedDate : { value : "#now#", cfsqltype : "timestamp" } 64 | }, 65 | { result : "local.result" } 66 | ); 67 | return local.result; 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v3/models/UserService.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * I am the User Service v3 3 | */ 4 | component 5 | extends="v3.models.BaseService" 6 | singleton 7 | accessors="true" 8 | { 9 | 10 | /** 11 | * Constructor 12 | */ 13 | UserService function init(){ 14 | super.init( 15 | entityName = "user", 16 | tableName = "users", 17 | moduleName = "v3" 18 | ) 19 | return this; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v4/ModuleConfig.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * v4 Module Config 3 | */ 4 | component { 5 | 6 | // Module Properties 7 | this.title = "v4"; 8 | // Module Entry Point 9 | this.entryPoint = "v4"; 10 | // Inherit entry point from parent, so this will be /api/v1 11 | this.inheritEntryPoint = true; 12 | // Model Namespace 13 | this.modelNamespace = "v4"; 14 | // CF Mapping 15 | this.cfmapping = "v4"; 16 | // Module Dependencies 17 | this.dependencies = []; 18 | 19 | function configure(){ 20 | } 21 | 22 | /** 23 | * Fired when the module is registered and activated. 24 | */ 25 | function onLoad(){ 26 | } 27 | 28 | /** 29 | * Fired when the module is unregistered and unloaded 30 | */ 31 | function onUnload(){ 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v4/config/Router.cfc: -------------------------------------------------------------------------------- 1 | component { 2 | 3 | function configure(){ 4 | // API Based Resourceful Routes: 5 | // https://coldbox.ortusbooks.com/the-basics/routing/routing-dsl/resourceful-routes#api-resourceful-routes 6 | apiResources( resource = "rants", parameterName = "rantId" ); 7 | 8 | // Entry Point 9 | route( "/", "echo.index" ); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /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="coldbox.system.RestHandler" { 5 | 6 | // REST Allowed HTTP Methods Ex: this.allowedMethods = {delete='POST,DELETE',index='GET'} 7 | this.allowedMethods = {}; 8 | 9 | /** 10 | * Index 11 | */ 12 | any function index( event, rc, prc ){ 13 | prc.response.setData( "Welcome to my ColdBox RESTFul Service v4" ); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v4/handlers/Rants.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * My RESTFul Rants Event Handler which inherits from the module `api` 3 | * Since we inherit from the RestHandler we get lots of goodies like automatic HTTP method protection, 4 | * missing routes, invalid routes, and much more. 5 | * 6 | * @see https://coldbox.ortusbooks.com/digging-deeper/rest-handler 7 | * @see https://coldbox.ortusbooks.com/digging-deeper/rest-handler#rest-handler-security 8 | */ 9 | component extends="coldbox.system.RestHandler" { 10 | 11 | // DI 12 | property name="rantService" inject="RantService@v4"; 13 | property name="userService" inject="UserService@v4"; 14 | 15 | /** 16 | * Returns a list of Rants 17 | */ 18 | any function index( event, rc, prc ){ 19 | prc.response.setData( rantService.list().map( rant => rant.getMemento() ) ); 20 | } 21 | 22 | /** 23 | * Returns a single Rant 24 | */ 25 | function show( event, rc, prc ){ 26 | validateOrFail( target = rc, constraints = { rantId : { required : true, type : "uuid" } } ); 27 | prc.response.setData( rantService.getOrFail( rc.rantId ).getMemento() ); 28 | } 29 | 30 | /** 31 | * Deletes a single Rant 32 | */ 33 | function delete( event, rc, prc ){ 34 | validateOrFail( target = rc, constraints = { rantId : { required : true, type : "uuid" } } ); 35 | rantService.existsOrFail( rc.rantId ); 36 | rantService.delete( rc.rantId ); 37 | prc.response.addMessage( "Rant deleted" ); 38 | } 39 | 40 | /** 41 | * Creates a new Rant 42 | */ 43 | function create( event, rc, prc ){ 44 | var rant = rantService.new(); 45 | 46 | validateOrFail( target = rc, constraints = rant.constraints ); 47 | 48 | userService.existsOrFail( rc.userId ); 49 | 50 | rant.setBody( rc.body ); 51 | rant.setUserId( rc.userId ); 52 | 53 | rantService.create( rant ); 54 | 55 | prc.response.setData( rant.getMemento() ); 56 | prc.response.addMessage( "Rant created" ); 57 | } 58 | 59 | /** 60 | * Updates an Existing Rant 61 | */ 62 | function update( event, rc, prc ){ 63 | validateOrFail( target = rc, constraints = { rantId : { required : true, type : "uuid" } } ); 64 | 65 | var rant = rantService.getOrFail( rc.rantId ); 66 | 67 | validateOrFail( target = rc, constraints = rant.constraints ); 68 | 69 | userService.existsOrFail( rc.userId ); 70 | 71 | rant.setBody( rc.body ); 72 | rant.setuserId( rc.userId ); 73 | 74 | rantService.update( rant ); 75 | 76 | prc.response.setData( rant.getMemento() ); 77 | prc.response.addMessage( "Rant Updated" ); 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v4/models/BaseService.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * I am the Base Service v4 3 | */ 4 | component accessors="true" { 5 | 6 | // DI 7 | property name="populator" inject="wirebox:populator"; 8 | property name="wirebox" inject="wirebox"; 9 | 10 | // Properties 11 | property name="entityName"; 12 | property name="tableName"; 13 | property name="primaryKey"; 14 | property name="serviceName"; 15 | property name="moduleName"; 16 | 17 | function init( 18 | entityName, 19 | tableName, 20 | primaryKey = "id", 21 | serviceName = "#arguments.entityName#Service", 22 | moduleName = "" 23 | ){ 24 | setEntityName( arguments.entityName ); 25 | setTableName( arguments.tableName ); 26 | setPrimaryKey( arguments.primaryKey ); 27 | setServiceName( arguments.serviceName ); 28 | setModuleName( arguments.moduleName ); 29 | return this; 30 | } 31 | 32 | /** 33 | * Check to see if there is a row with a matching primary key in the database. 34 | * Much faster than a full entity query and object load 35 | * 36 | * @id The primary key id to verify 37 | * 38 | * @return Returns true if there is a row with the matching Primary Key, otherwise returns false 39 | */ 40 | boolean function exists( required id ){ 41 | return booleanFormat( 42 | queryExecute( 43 | "select #getPrimaryKey()# from #getTableName()# 44 | where #getPrimaryKey()# = :id", 45 | { id : arguments.id } 46 | ).len() 47 | ) 48 | } 49 | 50 | /** 51 | * Get a new entity object according to entity name 52 | */ 53 | function new(){ 54 | return wirebox.getInstance( "#getEntityname()#@#getModuleName()#" ); 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 | * @id The primary key id to verify 61 | * 62 | * @return Returns true if there is a row with the matching Primary Key 63 | * 64 | * @throws EntityNotFound if the entity is not found 65 | */ 66 | boolean function existsOrFail( required id ){ 67 | if ( exists( argumentCollection = arguments ) ) { 68 | return true; 69 | } 70 | throw( type = "EntityNotFound", message = "#getEntityName()# Not Found" ); 71 | } 72 | 73 | /** 74 | * Query and load an entity if possible, else throw an error 75 | * 76 | * @id The primary key id to retrieve 77 | * 78 | * @return Returns the Entity if there is a row with the matching Primary Key 79 | * 80 | * @throws EntityNotFound if the entity is not found 81 | */ 82 | function getOrFail( required id ){ 83 | var maybeEntity = this.get( arguments.id ); 84 | if ( !maybeEntity.isLoaded() ) { 85 | throw( type = "EntityNotFound", message = "#getEntityName()# Not Found" ); 86 | } 87 | return maybeEntity; 88 | } 89 | 90 | /** 91 | * Try to get an entity from the requested id 92 | * 93 | * @id The primary key id to retrieve 94 | * 95 | * @return Returns the Entity if there is a row with the matching Primary Key or an empty entity if not found 96 | */ 97 | function get( required id ){ 98 | return queryExecute( 99 | "select * from #getTableName()# 100 | where #getPrimaryKey()# = :id 101 | ", 102 | { id : arguments.id } 103 | ).reduce( ( result, row ) => populator.populateFromStruct( result, row ), new () ); 104 | } 105 | 106 | /** 107 | * Base delete entity 108 | */ 109 | function delete( required id ){ 110 | queryExecute( 111 | "delete from #getTableName()# 112 | where #getPrimaryKey()# = :id 113 | ", 114 | { id : arguments.id }, 115 | { result : "local.result" } 116 | ); 117 | return local.result; 118 | } 119 | 120 | } 121 | -------------------------------------------------------------------------------- /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="updatedDate" type="date"; 14 | property name="userId" type="string"; 15 | 16 | // Validation Constraints 17 | this.constraints = { 18 | body : { required : true }, 19 | userId : { required : true, type : "uuid" } 20 | }; 21 | 22 | /** 23 | * Constructor 24 | */ 25 | Rant function init(){ 26 | var now = now(); 27 | variables.createdDate = now; 28 | variables.updatedDate = now; 29 | return this; 30 | } 31 | 32 | /** 33 | * get the user 34 | */ 35 | function getUser(){ 36 | return userService.get( getUserId() ); 37 | } 38 | 39 | /** 40 | * isLoaded 41 | */ 42 | boolean function isLoaded(){ 43 | return ( !isNull( variables.id ) && len( variables.id ) ); 44 | } 45 | 46 | /** 47 | * Marshall my object to data 48 | */ 49 | function getMemento(){ 50 | return { 51 | "rantId" : getId(), 52 | "body" : getBody(), 53 | "createdDate" : dateFormat( getCreatedDate(), "long" ), 54 | "updatedDate" : dateFormat( getUpdatedDate(), "long" ), 55 | "userId" : getuserId() 56 | }; 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v4/models/RantService.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * I am the Rant Service v4 3 | */ 4 | component 5 | extends="v4.models.BaseService" 6 | singleton 7 | accessors="true" 8 | { 9 | 10 | /** 11 | * Constructor 12 | */ 13 | RantService function init(){ 14 | super.init( 15 | entityName = "Rant", 16 | tableName = "rants", 17 | moduleName = "v4" 18 | ) 19 | return this; 20 | } 21 | 22 | /** 23 | * Let WireBox build new Rant objects for me 24 | */ 25 | Rant function new() provider="Rant@v4"{ 26 | } 27 | 28 | array function list(){ 29 | return this 30 | .listArray() 31 | .map( function( rant ){ 32 | return populator.populateFromStruct( new (), rant ); 33 | } ); 34 | } 35 | 36 | array function listArray(){ 37 | return queryExecute( 38 | "select * from rants ORDER BY createdDate DESC", 39 | {}, 40 | { returnType : "array" } 41 | ); 42 | } 43 | 44 | function create( required Rant rant ){ 45 | var now = now(); 46 | arguments.rant.setId( createUUID() ); 47 | 48 | queryExecute( 49 | "insert into rants 50 | set 51 | id = :rantId, 52 | body = :body, 53 | userId = :userId 54 | ", 55 | { 56 | rantId : arguments.rant.getId(), 57 | body : { 58 | value : "#arguments.rant.getBody()#", 59 | cfsqltype : "longvarchar" 60 | }, 61 | userId : arguments.rant.getuserId() 62 | } 63 | ); 64 | return arguments.rant; 65 | } 66 | 67 | function update( required Rant rant ){ 68 | arguments.rant.setUpdatedDate( now() ); 69 | queryExecute( 70 | "update rants 71 | set 72 | body = :body, 73 | updatedDate = :updatedDate 74 | where id = :rantId 75 | ", 76 | { 77 | rantId : arguments.rant.getID(), 78 | body : { 79 | value : "#arguments.rant.getBody()#", 80 | cfsqltype : "longvarchar" 81 | }, 82 | updatedDate : { 83 | value : "#arguments.rant.getUpdatedDate()#", 84 | cfsqltype : "timestamp" 85 | } 86 | } 87 | ); 88 | return arguments.rant; 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v4/models/User.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * I am a new User Object 3 | */ 4 | component accessors="true" { 5 | 6 | // Properties 7 | property name="id" type="string"; 8 | property name="username" type="string"; 9 | property name="email" type="string"; 10 | property name="password" type="string"; 11 | property name="createdDate" type="date"; 12 | property name="updatedDate" type="date"; 13 | 14 | // Validation Constraints 15 | this.constraints = { 16 | username : { required : true }, 17 | email : { required : true }, 18 | password : { required : true } 19 | }; 20 | 21 | /** 22 | * Constructor 23 | */ 24 | User function init(){ 25 | var now = now(); 26 | variables.createdDate = now; 27 | variables.updatedDate = now; 28 | return this; 29 | } 30 | 31 | /** 32 | * isLoaded 33 | */ 34 | boolean function isLoaded(){ 35 | return ( !isNull( variables.id ) && len( variables.id ) ); 36 | } 37 | 38 | /** 39 | * Marshall my object to data 40 | */ 41 | function getMemento(){ 42 | return { 43 | "id" : getID(), 44 | "username" : getUsername(), 45 | "email" : getEmail(), 46 | "createdDate" : dateFormat( getCreatedDate(), "long" ), 47 | "updatedDate" : dateFormat( getUpdatedDate(), "long" ) 48 | }; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v4/models/UserService.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * I am the User Service V4 3 | */ 4 | component 5 | extends="v4.models.BaseService" 6 | singleton 7 | accessors="true" 8 | { 9 | 10 | /** 11 | * Constructor 12 | */ 13 | UserService function init(){ 14 | super.init( 15 | entityName = "User", 16 | tableName = "users", 17 | moduleName = "v4" 18 | ) 19 | return this; 20 | } 21 | 22 | /** 23 | * Let WireBox build new Rant objects for me 24 | */ 25 | User function new() provider="User@v4"{ 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v5/ModuleConfig.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * v5 Module Config 3 | */ 4 | component { 5 | 6 | // Module Properties 7 | this.title = "v5"; 8 | this.author = ""; 9 | this.webURL = ""; 10 | this.description = ""; 11 | this.version = "1.0.0"; 12 | // If true, looks for views in the parent first, if not found, then in the module. Else vice-versa 13 | this.viewParentLookup = true; 14 | // If true, looks for layouts in the parent first, if not found, then in module. Else vice-versa 15 | this.layoutParentLookup = true; 16 | // Module Entry Point 17 | this.entryPoint = "v5"; 18 | // Inherit entry point from parent, so this will be /api/v1 19 | this.inheritEntryPoint = true; 20 | // Model Namespace 21 | this.modelNamespace = "v5"; 22 | // CF Mapping 23 | this.cfmapping = "v5"; 24 | // Auto-map models 25 | this.autoMapModels = true; 26 | // Module Dependencies 27 | this.dependencies = []; 28 | 29 | function configure(){ 30 | // parent settings 31 | parentSettings = {}; 32 | 33 | // module settings - stored in modules.name.settings 34 | settings = {}; 35 | 36 | // Layout Settings 37 | layoutSettings = { defaultLayout : "" }; 38 | 39 | // SES Routes: config/Router.cfc 40 | 41 | // Custom Declared Points 42 | interceptorSettings = { customInterceptionPoints : "" }; 43 | 44 | // Custom Declared Interceptors 45 | interceptors = []; 46 | 47 | // Binder Mappings 48 | // binder.map("Alias").to("#moduleMapping#.model.MyService"); 49 | } 50 | 51 | /** 52 | * Fired when the module is registered and activated. 53 | */ 54 | function onLoad(){ 55 | } 56 | 57 | /** 58 | * Fired when the module is unregistered and unloaded 59 | */ 60 | function onUnload(){ 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v5/config/Router.cfc: -------------------------------------------------------------------------------- 1 | component { 2 | 3 | function configure(){ 4 | // API Based Resourceful Routes: 5 | // https://coldbox.ortusbooks.com/the-basics/routing/routing-dsl/resourceful-routes#api-resourceful-routes 6 | apiResources( resource = "rants", parameterName = "rantId" ); 7 | 8 | // Entry Point 9 | route( "/", "echo.index" ); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /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="coldbox.system.RestHandler" { 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 | * Since we inherit from the RestHandler we get lots of goodies like automatic HTTP method protection, 4 | * missing routes, invalid routes, and much more. 5 | * 6 | * @see https://coldbox.ortusbooks.com/digging-deeper/rest-handler 7 | * @see https://coldbox.ortusbooks.com/digging-deeper/rest-handler#rest-handler-security 8 | */ 9 | component extends="coldbox.system.RestHandler" { 10 | 11 | // DI 12 | property name="rantService" inject="RantService@v5"; 13 | property name="userService" inject="UserService@v5"; 14 | 15 | this.prehandler_only = "show,delete,update"; 16 | any function preHandler( event, rc, prc, action, eventArguments ){ 17 | param rc.rantId = ""; 18 | } 19 | 20 | /** 21 | * Returns a list of Rants 22 | */ 23 | any function index( event, rc, prc ){ 24 | prc.response.setData( rantService.list().map( ( rant ) => rant.getMemento() ) ); 25 | } 26 | 27 | /** 28 | * Returns a single Rant 29 | */ 30 | function show( event, rc, prc ){ 31 | prc.response.setData( rantService.getOrFail( rc.rantId ).getMemento() ); 32 | } 33 | 34 | /** 35 | * Deletes a single Rant 36 | */ 37 | function delete( event, rc, prc ){ 38 | rantService.getOrFail( rc.rantId ).delete(); 39 | prc.response.addMessage( "Rant deleted" ); 40 | } 41 | 42 | /** 43 | * Creates a new Rant 44 | */ 45 | function create( event, rc, prc ){ 46 | var rant = rantService 47 | .new( rc ) 48 | .validateOrFail() 49 | .save(); 50 | prc.response.setData( rant.getMemento() ).addMessage( "Rant created" ); 51 | } 52 | 53 | /** 54 | * Updates an Existing Rant 55 | * 56 | */ 57 | function update( event, rc, prc ){ 58 | var rant = rantService 59 | .getOrFail( rc.rantId ) 60 | .populate( memento = rc, exclude = "id" ) 61 | .validateOrFail() 62 | .save(); 63 | prc.response.setData( rant.getMemento() ).addMessage( "Rant Updated" ); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v5/models/BaseEntity.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * I am a base entity which exposes some very cool methods to provide fluency and readability 3 | * while still encapsulating them in a service. 4 | */ 5 | component accessors="true" { 6 | 7 | // Entity metadata + service 8 | property name="_primaryKey"; 9 | property name="_entityName"; 10 | property name="_serviceName"; 11 | property name="_moduleName"; 12 | property name="_entityService"; 13 | 14 | // DI Injection 15 | property name="wirebox" inject="wirebox"; 16 | property name="populator" inject="wirebox:populator"; 17 | property name="coldbox" inject="coldbox"; 18 | 19 | // Basic memento settings 20 | this.memento = { 21 | // An array of the properties/relationships to include by default 22 | defaultIncludes : [ "*" ], 23 | // An array of properties/relationships to exclude by default 24 | defaultExcludes : [], 25 | // An array of properties/relationships to NEVER include 26 | neverInclude : [ 27 | "password", 28 | "_primaryKey", 29 | "_entityName", 30 | "_serviceName", 31 | "_moduleName", 32 | "_entityService" 33 | ], 34 | // A struct of defaults for properties/relationships if they are null 35 | defaults : {}, 36 | // A struct of mapping functions for properties/relationships that can transform them 37 | mappers : {} 38 | } 39 | 40 | /** 41 | * Initialize Entity - stores information needed 42 | * 43 | * @primaryKey The name of the primary key field in the entity 44 | * @entityName The name of the entity so we can reference it for calls to related DAO and Service. Set as optional for backwards 45 | * @moduleName The name of the module for the objects 46 | * @serviceName The name of the service that manages the entity 47 | */ 48 | function init( 49 | primaryKey = "id", 50 | entityName = "", 51 | moduleName = "v5", 52 | serviceName = "#arguments.entityName#Service@#arguments.moduleName#" 53 | ){ 54 | variables._primaryKey = arguments.primaryKey; 55 | variables._entityName = arguments.entityName; 56 | variables._moduleName = arguments.moduleName; 57 | variables._serviceName = arguments.serviceName; 58 | 59 | return this; 60 | } 61 | 62 | /** 63 | * Once this entity is loaded, load up it's companion service 64 | */ 65 | function onDIComplete(){ 66 | variables._entityService = wirebox.getInstance( variables._serviceName ); 67 | } 68 | 69 | /** 70 | * Verify if entity is loaded or not 71 | */ 72 | boolean function isLoaded(){ 73 | return ( 74 | !structKeyExists( variables, variables._primaryKey ) OR !len( variables[ variables._primaryKey ] ) ? false : true 75 | ); 76 | } 77 | 78 | /** 79 | * Get the primary key value of this object. 80 | * 81 | * @return The primary key or an empty value if not set. 82 | */ 83 | function getId(){ 84 | return isLoaded() ? variables[ variables._primaryKey ] : ""; 85 | } 86 | 87 | /** 88 | * Populate a model object from the request Collection or a passed in memento structure 89 | * 90 | * @scope Use scope injection instead of setters population. Ex: scope=variables.instance. 91 | * @trustedSetter If set to true, the setter method will be called even if it does not exist in the object 92 | * @include A list of keys to include in the population 93 | * @exclude A list of keys to exclude in the population 94 | * @ignoreEmpty Ignore empty values on populations, great for ORM population 95 | * @nullEmptyInclude A list of keys to NULL when empty 96 | * @nullEmptyExclude A list of keys to NOT NULL when empty 97 | * @composeRelationships Automatically attempt to compose relationships from memento 98 | * @memento A structure to populate the model, if not passed it defaults to the request collection 99 | * @jsonstring If you pass a json string, we will populate your model with it 100 | * @xml If you pass an xml string, we will populate your model with it 101 | * @qry If you pass a query, we will populate your model with it 102 | * @rowNumber The row of the qry parameter to populate your model with 103 | */ 104 | function populate( 105 | struct memento = coldbox 106 | .getRequestService() 107 | .getContext() 108 | .getCollection(), 109 | scope = "", 110 | boolean trustedSetter = false, 111 | include = "", 112 | exclude = "", 113 | boolean ignoreEmpty = false, 114 | nullEmptyInclude = "", 115 | nullEmptyExclude = "", 116 | boolean composeRelationships = false, 117 | string jsonstring, 118 | string xml, 119 | query qry 120 | ){ 121 | // Seed the target for population 122 | arguments[ "target" ] = this; 123 | 124 | // json? 125 | if ( !isNull( arguments.jsonstring ) ) { 126 | return variables.populator.populateFromJSON( argumentCollection = arguments ); 127 | } 128 | // XML 129 | else if ( !isNull( arguments.xml ) ) { 130 | return variables.populator.populateFromXML( argumentCollection = arguments ); 131 | } 132 | // Query 133 | else if ( !isNull( arguments.qry ) ) { 134 | return variables.populator.populateFromQuery( argumentCollection = arguments ); 135 | } 136 | // Mementos 137 | else { 138 | // populate 139 | return variables.populator.populateFromStruct( argumentCollection = arguments ); 140 | } 141 | } 142 | 143 | /** 144 | * Validate an object or structure according to the constraints rules and throw an exception if the validation fails. 145 | * The validation errors will be contained in the `extendedInfo` of the exception in JSON format 146 | */ 147 | function validateOrFail(){ 148 | arguments.target = this; 149 | return wirebox 150 | .getInstance( "ValidationManager@cbvalidation" ) 151 | .validateOrFail( argumentCollection = arguments ); 152 | } 153 | 154 | /** 155 | * Save an entity 156 | */ 157 | function save(){ 158 | if ( isLoaded() ) { 159 | return variables._entityService.update( this ); 160 | } else { 161 | return variables._entityService.create( this ); 162 | } 163 | } 164 | 165 | /** 166 | * Delete an entity 167 | */ 168 | function delete(){ 169 | return variables._entityService.delete( this.getId() ); 170 | } 171 | 172 | } 173 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v5/models/BaseService.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * I am the Base Service v5 3 | */ 4 | component accessors="true" { 5 | 6 | // DI 7 | property name="wirebox" inject="wirebox"; 8 | property name="populator" inject="wirebox:populator"; 9 | 10 | // Properties 11 | property name="entityName"; 12 | property name="tableName"; 13 | property name="primaryKey"; 14 | property name="serviceName"; 15 | property name="moduleName"; 16 | 17 | /** 18 | * Constructorhonduras 19 | * 20 | * @entityName The name of the entity so we can reference it for calls to related DAO and Service. Set as optional for backwards 21 | * @tableName The table name this service is bound to 22 | * @primaryKey The primary key to use for operations 23 | * @moduleName The name of the module for the objects 24 | * @serviceName The name of the service that manages the entity 25 | */ 26 | function init( 27 | entityName, 28 | tableName, 29 | primaryKey = "id", 30 | moduleName = "v5", 31 | serviceName = "#arguments.entityName#Service@#arguments.moduleName#" 32 | ){ 33 | setEntityName( arguments.entityName ); 34 | setTableName( arguments.tableName ); 35 | setPrimaryKey( arguments.primaryKey ); 36 | setServiceName( arguments.serviceName ); 37 | setModuleName( arguments.moduleName ); 38 | 39 | return this; 40 | } 41 | 42 | /** 43 | * Get a new entity object according to entity name 44 | * 45 | * @properties The initial properties to populate the entity with 46 | * 47 | * @return A brand spanking new entity object 48 | */ 49 | function new( struct properties = {} ){ 50 | var entity = wirebox.getInstance( "#getEntityname()#@#getModuleName()#" ); 51 | return populator.populateFromStruct( target = entity, memento = arguments.properties ); 52 | } 53 | 54 | /** 55 | * Check to see if there is a row with a matching primary key in the database. 56 | * Much faster than a full entity query and object load 57 | * 58 | * @id The primary key id to verify 59 | * 60 | * @return Returns true if there is a row with the matching Primary Key, otherwise returns false 61 | */ 62 | boolean function exists( required id ){ 63 | return booleanFormat( 64 | queryExecute( 65 | "select #getPrimaryKey()# from #getTableName()# 66 | where #getPrimaryKey()# = :id", 67 | { id : arguments.id } 68 | ).len() 69 | ) 70 | } 71 | 72 | /** 73 | * 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 74 | * 75 | * @id The primary key id to verify 76 | * 77 | * @return Returns true if there is a row with the matching Primary Key 78 | * 79 | * @throws EntityNotFound if the entity is not found 80 | */ 81 | boolean function existsOrFail( required id ){ 82 | if ( exists( argumentCollection = arguments ) ) { 83 | return true; 84 | } 85 | throw( type = "EntityNotFound", message = "#getEntityName()# Not Found" ); 86 | } 87 | 88 | /** 89 | * Query and load an entity if possible, else throw an error 90 | * 91 | * @id The primary key id to retrieve 92 | * 93 | * @return Returns the Entity if there is a row with the matching Primary Key 94 | * 95 | * @throws EntityNotFound if the entity is not found 96 | */ 97 | function getOrFail( required id ){ 98 | var maybeEntity = this.get( arguments.id ); 99 | if ( !maybeEntity.isLoaded() ) { 100 | throw( type = "EntityNotFound", message = "#getEntityName()# Not Found" ); 101 | } 102 | return maybeEntity; 103 | } 104 | 105 | /** 106 | * Try to get an entity from the requested id 107 | * 108 | * @id The primary key id to retrieve 109 | * 110 | * @return Returns the Entity if there is a row with the matching Primary Key or an empty entity if not found 111 | */ 112 | function get( required id ){ 113 | return queryExecute( 114 | "select * from #getTableName()# 115 | where #getPrimaryKey()# = :id 116 | ", 117 | { id : arguments.id } 118 | ).reduce( ( result, row ) => populator.populateFromStruct( result, row ), new () ); 119 | } 120 | 121 | /** 122 | * Base delete entity 123 | */ 124 | function delete( required id ){ 125 | queryExecute( 126 | "delete from #getTableName()# 127 | where #getPrimaryKey()# = :id 128 | ", 129 | { id : arguments.id }, 130 | { result : "local.result" } 131 | ); 132 | return local.result; 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /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="updatedDate" type="date"; 14 | property name="userId" type="string"; 15 | 16 | // Validation Constraints 17 | this.constraints = { 18 | body : { required : true }, 19 | userId : { 20 | required : true, 21 | type : "uuid", 22 | udf : ( value, target ) => { 23 | if ( isNull( arguments.value ) || !isValid( "uuid", arguments.value ) ) return false; 24 | return userService.exists( arguments.value ); 25 | }, 26 | udfMessage : "User ({rejectedValue}) not found" 27 | } 28 | }; 29 | 30 | /** 31 | * Constructor 32 | */ 33 | Rant function init(){ 34 | return super.init( entityName = "rant" ); 35 | } 36 | 37 | /** 38 | * Get related user object 39 | */ 40 | function getUser(){ 41 | return userService.get( getUserId() ); 42 | } 43 | 44 | /** 45 | * Does this rant have a user assigned to it already 46 | */ 47 | boolean function hasUser(){ 48 | return !isNull( variables.userId ) && len( variables.userId ); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v5/models/RantService.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * I am the Rant Service v5 3 | */ 4 | component 5 | extends="v5.models.BaseService" 6 | singleton 7 | accessors="true" 8 | { 9 | 10 | /** 11 | * Constructor 12 | */ 13 | RantService function init(){ 14 | super.init( 15 | entityName = "Rant", 16 | tableName = "rants", 17 | parameterName = "rantId" 18 | ) 19 | return this; 20 | } 21 | 22 | array function list(){ 23 | return this 24 | .listArray() 25 | .map( function( rant ){ 26 | return populator.populateFromStruct( new (), rant ); 27 | } ); 28 | } 29 | 30 | array function listArray(){ 31 | return queryExecute( 32 | "select * from rants ORDER BY createdDate DESC", 33 | {}, 34 | { returnType : "array" } 35 | ); 36 | } 37 | 38 | function create( required Rant rant ){ 39 | var now = now(); 40 | arguments.rant.setId( createUUID() ); 41 | 42 | queryExecute( 43 | "insert into rants 44 | set 45 | id = :rantId, 46 | body = :body, 47 | userId = :userId 48 | ", 49 | { 50 | rantId : arguments.rant.getId(), 51 | body : { 52 | value : "#arguments.rant.getBody()#", 53 | cfsqltype : "longvarchar" 54 | }, 55 | userId : arguments.rant.getuserId() 56 | } 57 | ); 58 | return arguments.rant; 59 | } 60 | 61 | function update( required Rant rant ){ 62 | arguments.rant.setUpdatedDate( now() ); 63 | queryExecute( 64 | "update rants 65 | set 66 | body = :body, 67 | updatedDate = :updatedDate 68 | where id = :rantId 69 | ", 70 | { 71 | rantId : arguments.rant.getID(), 72 | body : { 73 | value : "#arguments.rant.getBody()#", 74 | cfsqltype : "longvarchar" 75 | }, 76 | updatedDate : { 77 | value : "#arguments.rant.getUpdatedDate()#", 78 | cfsqltype : "timestamp" 79 | } 80 | } 81 | ); 82 | return arguments.rant; 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v5/models/User.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * I am a new User Object 3 | */ 4 | component extends="v5.models.BaseEntity" accessors="true" { 5 | 6 | // Properties 7 | property name="id" type="string"; 8 | property name="username" type="string"; 9 | property name="email" type="string"; 10 | property name="password" type="string"; 11 | property name="createdDate" type="date"; 12 | property name="updatedDate" type="date"; 13 | 14 | // Validation Constraints 15 | this.constraints = { 16 | username : { required : true }, 17 | email : { required : true }, 18 | password : { required : true } 19 | }; 20 | 21 | /** 22 | * Constructor 23 | */ 24 | User function init(){ 25 | return super.init( entityName = "User" ); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v5/models/UserService.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * I am the User Service v5 3 | */ 4 | component 5 | extends="v5.models.BaseService" 6 | singleton 7 | accessors="true" 8 | { 9 | 10 | /** 11 | * Constructor 12 | */ 13 | UserService function init(){ 14 | super.init( 15 | entityName = "User", 16 | tableName = "users", 17 | parameterName = "userId" 18 | ) 19 | return this; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v6/ModuleConfig.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * v6 Module Config 3 | */ 4 | component { 5 | 6 | // Module Properties 7 | this.title = "v6"; 8 | this.author = ""; 9 | this.webURL = ""; 10 | this.description = ""; 11 | this.version = "1.0.0"; 12 | // If true, looks for views in the parent first, if not found, then in the module. Else vice-versa 13 | this.viewParentLookup = true; 14 | // If true, looks for layouts in the parent first, if not found, then in module. Else vice-versa 15 | this.layoutParentLookup = true; 16 | // Module Entry Point 17 | this.entryPoint = "v6"; 18 | // Inherit entry point from parent, so this will be /api/v1 19 | this.inheritEntryPoint = true; 20 | // Model Namespace 21 | this.modelNamespace = "v6"; 22 | // CF Mapping 23 | this.cfmapping = "v6"; 24 | // Auto-map models 25 | this.autoMapModels = true; 26 | // Module Dependencies 27 | this.dependencies = []; 28 | 29 | function configure(){ 30 | // parent settings 31 | parentSettings = {}; 32 | 33 | // module settings - stored in modules.name.settings 34 | settings = {}; 35 | 36 | // Layout Settings 37 | layoutSettings = { defaultLayout : "" }; 38 | 39 | // SES Routes: config/Router.cfc 40 | 41 | // Custom Declared Points 42 | interceptorSettings = { customInterceptionPoints : "" }; 43 | 44 | // Custom Declared Interceptors 45 | interceptors = []; 46 | 47 | // Binder Mappings 48 | // binder.map("Alias").to("#moduleMapping#.model.MyService"); 49 | } 50 | 51 | /** 52 | * Fired when the module is registered and activated. 53 | */ 54 | function onLoad(){ 55 | } 56 | 57 | /** 58 | * Fired when the module is unregistered and unloaded 59 | */ 60 | function onUnload(){ 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v6/config/Router.cfc: -------------------------------------------------------------------------------- 1 | component { 2 | 3 | function configure(){ 4 | // API Based Resourceful Routes: 5 | // https://coldbox.ortusbooks.com/the-basics/routing/routing-dsl/resourceful-routes#api-resourceful-routes 6 | apiResources( resource = "rants", parameterName = "rantId" ); 7 | 8 | // Catch All Invalid Routes 9 | route( "/:anything", "Echo.onInvalidRoute" ); 10 | 11 | // Entry Point 12 | route( "/", "echo.index" ); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /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="coldbox.system.RestHandler" { 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 | * Since we inherit from the RestHandler we get lots of goodies like automatic HTTP method protection, 4 | * missing routes, invalid routes, and much more. 5 | * 6 | * @see https://coldbox.ortusbooks.com/digging-deeper/rest-handler 7 | * @see https://coldbox.ortusbooks.com/digging-deeper/rest-handler#rest-handler-security 8 | */ 9 | component extends="coldbox.system.RestHandler" { 10 | 11 | // DI 12 | property name="rantService" inject="RantService@v6"; 13 | property name="userService" inject="UserService@v6"; 14 | 15 | /** 16 | * Param global incoming variables for the endpoint 17 | */ 18 | any function preHandler( event, rc, prc, action, eventArguments ){ 19 | param rc.rantId = ""; 20 | param rc.includes = ""; 21 | param rc.excludes = ""; 22 | param rc.ignoreDefaults = false; 23 | } 24 | 25 | /** 26 | * Returns a list of Rants 27 | * 28 | * @x-route (GET) /api/v6/rants 29 | * @response-200 ~api-v6/Rants/index/responses.json##200 30 | */ 31 | any function index( event, rc, prc ) cache=true cacheTimeout=60{ 32 | prc.response.setData( 33 | rantService 34 | .list() 35 | .map( ( rant ) => rant.getMemento( 36 | includes : rc.includes, 37 | excludes : rc.excludes, 38 | ignoreDefaults: rc.ignoreDefaults 39 | ) ) 40 | ); 41 | } 42 | 43 | /** 44 | * Return a single Rant by id 45 | * 46 | * @x-route (GET) /api/v6/rants/:rantId 47 | * @x-parameters ~api-v6/Rants/show/parameters.json##parameters 48 | * @response-200 ~api-v6/Rants/show/responses.json##200 49 | * @response-404 ~_responses/rant.404.json 50 | */ 51 | function show( event, rc, prc ) cache=true cacheTimeout=60{ 52 | prc.response.setData( 53 | rantService 54 | .getOrFail( rc.rantId ) 55 | .getMemento( 56 | includes : rc.includes, 57 | excludes : rc.excludes, 58 | ignoreDefaults: rc.ignoreDefaults 59 | ) 60 | ); 61 | } 62 | 63 | /** 64 | * Delete a single Rant. 65 | * 66 | * @x-route (DELETE) /api/v6/rants/:rantId 67 | * @x-parameters ~api-v6/Rants/delete/parameters.json##parameters 68 | * @response-200 ~api-v6/Rants/delete/responses.json##200 69 | * @response-404 ~_responses/rant.404.json 70 | */ 71 | function delete( event, rc, prc ){ 72 | rantService.getOrFail( rc.rantId ).delete(); 73 | prc.response.addMessage( "Rant deleted" ); 74 | getCache( "template" ).clearAllEvents(); 75 | } 76 | 77 | /** 78 | * Creates a new Rant. 79 | * 80 | * @x-route (POST) /api/v1/rants 81 | * @requestBody ~api-v6/Rants/create/requestBody.json 82 | * @response-200 ~api-v6/Rants/create/responses.json##200 83 | * @response-400 ~api-v6/Rants/create/responses.json##400 84 | */ 85 | function create( event, rc, prc ){ 86 | prc.response 87 | .setData( 88 | rantService 89 | .new( rc ) 90 | .validateOrFail() 91 | .save() 92 | .getMemento( 93 | includes : rc.includes, 94 | excludes : rc.excludes, 95 | ignoreDefaults: rc.ignoreDefaults 96 | ) 97 | ) 98 | .addMessage( "Rant created" ); 99 | 100 | getCache( "template" ).clearAllEvents(); 101 | } 102 | 103 | /** 104 | * Update an existing Rant. 105 | * 106 | * @x-route (PUT) /api/v1/rants/:rantId 107 | * @requestBody ~api-v6/Rants/update/requestBody.json 108 | * @response-200 ~api-v6/Rants/update/responses.json##200 109 | * @response-400 ~api-v6/Rants/update/responses.json##400 110 | */ 111 | function update( event, rc, prc ){ 112 | prc.response 113 | .setData( 114 | rantService 115 | .getOrFail( rc.rantId ) 116 | .populate( memento = rc, exclude = "id" ) 117 | .validateOrFail() 118 | .save() 119 | .getMemento( 120 | includes : rc.includes, 121 | excludes : rc.excludes, 122 | ignoreDefaults: rc.ignoreDefaults 123 | ) 124 | ) 125 | .addMessage( "Rant Updated" ); 126 | 127 | getCache( "template" ).clearAllEvents(); 128 | } 129 | 130 | } 131 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v6/models/BaseEntity.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * I am a base entity which exposes some very cool methods to provide fluency and readability 3 | * while still encapsulating them in a service. 4 | */ 5 | component accessors="true" { 6 | 7 | // Entity metadata + service 8 | property name="_primaryKey"; 9 | property name="_entityName"; 10 | property name="_serviceName"; 11 | property name="_moduleName"; 12 | property name="_entityService"; 13 | 14 | // DI Injection 15 | property name="wirebox" inject="wirebox"; 16 | property name="populator" inject="wirebox:populator"; 17 | property name="coldbox" inject="coldbox"; 18 | 19 | // Basic memento settings 20 | this.memento = { 21 | // An array of the properties/relationships to include by default 22 | defaultIncludes : [ "*" ], 23 | // An array of properties/relationships to exclude by default 24 | defaultExcludes : [], 25 | // An array of properties/relationships to NEVER include 26 | neverInclude : [ 27 | "password", 28 | "_primaryKey", 29 | "_entityName", 30 | "_serviceName", 31 | "_moduleName", 32 | "_entityService" 33 | ], 34 | // A struct of defaults for properties/relationships if they are null 35 | defaults : {}, 36 | // A struct of mapping functions for properties/relationships that can transform them 37 | mappers : {} 38 | } 39 | 40 | /** 41 | * Initialize Entity - stores information needed 42 | * 43 | * @primaryKey The name of the primary key field in the entity 44 | * @entityName The name of the entity so we can reference it for calls to related DAO and Service. Set as optional for backwards 45 | * @moduleName The name of the module for the objects 46 | * @serviceName The name of the service that manages the entity 47 | */ 48 | function init( 49 | primaryKey = "id", 50 | entityName = "", 51 | moduleName = "v6", 52 | serviceName = "#arguments.entityName#Service@#arguments.moduleName#" 53 | ){ 54 | variables._primaryKey = arguments.primaryKey; 55 | variables._entityName = arguments.entityName; 56 | variables._moduleName = arguments.moduleName; 57 | variables._serviceName = arguments.serviceName; 58 | 59 | return this; 60 | } 61 | 62 | /** 63 | * Once this entity is loaded, load up it's companion service 64 | */ 65 | function onDIComplete(){ 66 | variables._entityService = wirebox.getInstance( variables._serviceName ); 67 | } 68 | 69 | /** 70 | * Verify if entity is loaded or not 71 | */ 72 | boolean function isLoaded(){ 73 | return ( 74 | !structKeyExists( variables, variables._primaryKey ) OR !len( variables[ variables._primaryKey ] ) ? false : true 75 | ); 76 | } 77 | 78 | /** 79 | * Get the primary key value of this object. 80 | * 81 | * @return The primary key or an empty value if not set. 82 | */ 83 | function getId(){ 84 | return isLoaded() ? variables[ variables._primaryKey ] : ""; 85 | } 86 | 87 | /** 88 | * Populate a model object from the request Collection or a passed in memento structure 89 | * 90 | * @scope Use scope injection instead of setters population. Ex: scope=variables.instance. 91 | * @trustedSetter If set to true, the setter method will be called even if it does not exist in the object 92 | * @include A list of keys to include in the population 93 | * @exclude A list of keys to exclude in the population 94 | * @ignoreEmpty Ignore empty values on populations, great for ORM population 95 | * @nullEmptyInclude A list of keys to NULL when empty 96 | * @nullEmptyExclude A list of keys to NOT NULL when empty 97 | * @composeRelationships Automatically attempt to compose relationships from memento 98 | * @memento A structure to populate the model, if not passed it defaults to the request collection 99 | * @jsonstring If you pass a json string, we will populate your model with it 100 | * @xml If you pass an xml string, we will populate your model with it 101 | * @qry If you pass a query, we will populate your model with it 102 | * @rowNumber The row of the qry parameter to populate your model with 103 | */ 104 | function populate( 105 | struct memento = coldbox 106 | .getRequestService() 107 | .getContext() 108 | .getCollection(), 109 | scope = "", 110 | boolean trustedSetter = false, 111 | include = "", 112 | exclude = "", 113 | boolean ignoreEmpty = false, 114 | nullEmptyInclude = "", 115 | nullEmptyExclude = "", 116 | boolean composeRelationships = false, 117 | string jsonstring, 118 | string xml, 119 | query qry 120 | ){ 121 | // Seed the target for population 122 | arguments[ "target" ] = this; 123 | 124 | // json? 125 | if ( !isNull( arguments.jsonstring ) ) { 126 | return variables.populator.populateFromJSON( argumentCollection = arguments ); 127 | } 128 | // XML 129 | else if ( !isNull( arguments.xml ) ) { 130 | return variables.populator.populateFromXML( argumentCollection = arguments ); 131 | } 132 | // Query 133 | else if ( !isNull( arguments.qry ) ) { 134 | return variables.populator.populateFromQuery( argumentCollection = arguments ); 135 | } 136 | // Mementos 137 | else { 138 | // populate 139 | return variables.populator.populateFromStruct( argumentCollection = arguments ); 140 | } 141 | } 142 | 143 | /** 144 | * Validate an object or structure according to the constraints rules and throw an exception if the validation fails. 145 | * The validation errors will be contained in the `extendedInfo` of the exception in JSON format 146 | */ 147 | function validateOrFail(){ 148 | arguments.target = this; 149 | return wirebox 150 | .getInstance( "ValidationManager@cbvalidation" ) 151 | .validateOrFail( argumentCollection = arguments ); 152 | } 153 | 154 | /** 155 | * Save an entity 156 | */ 157 | function save(){ 158 | if ( isLoaded() ) { 159 | return variables._entityService.update( this ); 160 | } else { 161 | return variables._entityService.create( this ); 162 | } 163 | } 164 | 165 | /** 166 | * Delete an entity 167 | */ 168 | function delete(){ 169 | return variables._entityService.delete( this.getId() ); 170 | } 171 | 172 | } 173 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v6/models/BaseService.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * I am the Base Service v6 3 | */ 4 | component accessors="true" { 5 | 6 | // DI 7 | property name="wirebox" inject="wirebox"; 8 | property name="populator" inject="wirebox:populator"; 9 | 10 | // Properties 11 | property name="entityName"; 12 | property name="tableName"; 13 | property name="primaryKey"; 14 | property name="serviceName"; 15 | property name="moduleName"; 16 | 17 | /** 18 | * Constructorhonduras 19 | * 20 | * @entityName The name of the entity so we can reference it for calls to related DAO and Service. Set as optional for backwards 21 | * @tableName The table name this service is bound to 22 | * @primaryKey The primary key to use for operations 23 | * @moduleName The name of the module for the objects 24 | * @serviceName The name of the service that manages the entity 25 | */ 26 | function init( 27 | entityName, 28 | tableName, 29 | primaryKey = "id", 30 | moduleName = "v6", 31 | serviceName = "#arguments.entityName#Service@#arguments.moduleName#" 32 | ){ 33 | setEntityName( arguments.entityName ); 34 | setTableName( arguments.tableName ); 35 | setPrimaryKey( arguments.primaryKey ); 36 | setServiceName( arguments.serviceName ); 37 | setModuleName( arguments.moduleName ); 38 | 39 | return this; 40 | } 41 | 42 | /** 43 | * Get a new entity object according to entity name 44 | * 45 | * @properties The initial properties to populate the entity with 46 | * 47 | * @return A brand spanking new entity object 48 | */ 49 | function new( struct properties = {} ){ 50 | var entity = wirebox.getInstance( "#getEntityname()#@#getModuleName()#" ); 51 | return populator.populateFromStruct( target = entity, memento = arguments.properties ); 52 | } 53 | 54 | /** 55 | * Check to see if there is a row with a matching primary key in the database. 56 | * Much faster than a full entity query and object load 57 | * 58 | * @id The primary key id to verify 59 | * 60 | * @return Returns true if there is a row with the matching Primary Key, otherwise returns false 61 | */ 62 | boolean function exists( required id ){ 63 | return booleanFormat( 64 | queryExecute( 65 | "select #getPrimaryKey()# from #getTableName()# 66 | where #getPrimaryKey()# = :id", 67 | { id : arguments.id } 68 | ).len() 69 | ) 70 | } 71 | 72 | /** 73 | * 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 74 | * 75 | * @id The primary key id to verify 76 | * 77 | * @return Returns true if there is a row with the matching Primary Key 78 | * 79 | * @throws EntityNotFound if the entity is not found 80 | */ 81 | boolean function existsOrFail( required id ){ 82 | if ( exists( argumentCollection = arguments ) ) { 83 | return true; 84 | } 85 | throw( type = "EntityNotFound", message = "#getEntityName()# Not Found" ); 86 | } 87 | 88 | /** 89 | * Query and load an entity if possible, else throw an error 90 | * 91 | * @id The primary key id to retrieve 92 | * 93 | * @return Returns the Entity if there is a row with the matching Primary Key 94 | * 95 | * @throws EntityNotFound if the entity is not found 96 | */ 97 | function getOrFail( required id ){ 98 | var maybeEntity = this.get( arguments.id ); 99 | if ( !maybeEntity.isLoaded() ) { 100 | throw( type = "EntityNotFound", message = "#getEntityName()# Not Found" ); 101 | } 102 | return maybeEntity; 103 | } 104 | 105 | /** 106 | * Try to get an entity from the requested id 107 | * 108 | * @id The primary key id to retrieve 109 | * 110 | * @return Returns the Entity if there is a row with the matching Primary Key or an empty entity if not found 111 | */ 112 | function get( required id ){ 113 | return queryExecute( 114 | "select * from #getTableName()# 115 | where #getPrimaryKey()# = :id 116 | ", 117 | { id : arguments.id } 118 | ).reduce( ( result, row ) => populator.populateFromStruct( result, row ), new () ); 119 | } 120 | 121 | /** 122 | * Base delete entity 123 | */ 124 | function delete( required id ){ 125 | queryExecute( 126 | "delete from #getTableName()# 127 | where #getPrimaryKey()# = :id 128 | ", 129 | { id : arguments.id }, 130 | { result : "local.result" } 131 | ); 132 | return local.result; 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /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="updatedDate" type="date"; 14 | property name="userId" type="string"; 15 | 16 | // Validation Constraints 17 | this.constraints = { 18 | body : { required : true }, 19 | userId : { 20 | required : true, 21 | type : "uuid", 22 | udf : ( value, target ) => { 23 | if ( isNull( arguments.value ) || !isValid( "uuid", arguments.value ) ) return false; 24 | return userService.exists( arguments.value ); 25 | }, 26 | udfMessage : "User ({rejectedValue}) not found" 27 | } 28 | }; 29 | 30 | /** 31 | * Constructor 32 | */ 33 | Rant function init(){ 34 | super.init( entityName = "rant" ); 35 | 36 | // Custom Includes 37 | this.memento.defaultIncludes = [ 38 | "id:rantId", 39 | "body", 40 | "createdDate", 41 | "updatedDate", 42 | "user" 43 | ]; 44 | 45 | return this; 46 | } 47 | 48 | /** 49 | * Get related user object 50 | */ 51 | function getUser(){ 52 | return userService.get( getUserId() ); 53 | } 54 | 55 | /** 56 | * Does this rant have a user assigned to it already 57 | */ 58 | boolean function hasUser(){ 59 | return !isNull( variables.userId ) && len( variables.userId ); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v6/models/RantService.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * I am the Rant Service v6 3 | */ 4 | component 5 | extends="v6.models.BaseService" 6 | singleton 7 | accessors="true" 8 | { 9 | 10 | /** 11 | * Constructor 12 | */ 13 | RantService function init(){ 14 | super.init( 15 | entityName = "Rant", 16 | tableName = "rants", 17 | parameterName = "rantId" 18 | ) 19 | return this; 20 | } 21 | 22 | array function list(){ 23 | return this 24 | .listArray() 25 | .map( function( rant ){ 26 | return populator.populateFromStruct( new (), rant ); 27 | } ); 28 | } 29 | 30 | array function listArray(){ 31 | return queryExecute( 32 | "select * from rants ORDER BY createdDate DESC", 33 | {}, 34 | { returnType : "array" } 35 | ); 36 | } 37 | 38 | function create( required Rant rant ){ 39 | var now = now(); 40 | arguments.rant.setId( createUUID() ); 41 | 42 | queryExecute( 43 | "insert into rants 44 | set 45 | id = :rantId, 46 | body = :body, 47 | userId = :userId 48 | ", 49 | { 50 | rantId : arguments.rant.getId(), 51 | body : { 52 | value : "#arguments.rant.getBody()#", 53 | cfsqltype : "longvarchar" 54 | }, 55 | userId : arguments.rant.getuserId() 56 | } 57 | ); 58 | return arguments.rant; 59 | } 60 | 61 | function update( required Rant rant ){ 62 | arguments.rant.setUpdatedDate( now() ); 63 | queryExecute( 64 | "update rants 65 | set 66 | body = :body, 67 | updatedDate = :updatedDate 68 | where id = :rantId 69 | ", 70 | { 71 | rantId : arguments.rant.getID(), 72 | body : { 73 | value : "#arguments.rant.getBody()#", 74 | cfsqltype : "longvarchar" 75 | }, 76 | updatedDate : { 77 | value : "#arguments.rant.getUpdatedDate()#", 78 | cfsqltype : "timestamp" 79 | } 80 | } 81 | ); 82 | return arguments.rant; 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v6/models/User.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * I am a new User Object 3 | */ 4 | component extends="v6.models.BaseEntity" accessors="true" { 5 | 6 | // Properties 7 | property name="id" type="string"; 8 | property name="username" type="string"; 9 | property name="email" type="string"; 10 | property name="password" type="string"; 11 | property name="createdDate" type="date"; 12 | property name="updatedDate" type="date"; 13 | 14 | // Validation Constraints 15 | this.constraints = { 16 | username : { required : true }, 17 | email : { required : true }, 18 | password : { required : true } 19 | }; 20 | 21 | /** 22 | * Constructor 23 | */ 24 | User function init(){ 25 | super.init( entityName = "User" ); 26 | return this; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /modules_app/api/modules_app/v6/models/UserService.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * I am the User Service v6 3 | */ 4 | component 5 | extends="v6.models.BaseService" 6 | singleton 7 | accessors="true" 8 | { 9 | 10 | /** 11 | * Constructor 12 | */ 13 | UserService function init(){ 14 | super.init( 15 | entityName = "User", 16 | tableName = "users", 17 | parameterName = "userId" 18 | ) 19 | return this; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Modern Functional & Fluent CFML REST APIs 2 | 3 | This repo is a demo for our presentation on building modern functional & fluent CFML REST APIs. 4 | 5 | ## App Setup 6 | 7 | ### Install Dependencies 8 | 9 | Just use CommandBox and install them: `box install` 10 | 11 | ### Database setup 12 | 13 | The database needed for this is MySQL 5.7+ or 8+. 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 or you can use our migrations. 14 | 15 | First create the database `fluentapi` in your MySQL database and get some credentials ready for storage: 16 | 17 | ```bash 18 | # Seed your .env file with your db and credentials 19 | dotenv populate 20 | 21 | # reload the shell so the credentials are loaded 22 | reload 23 | 24 | # run our migrations 25 | migrate up 26 | ``` 27 | 28 | ### Start the app 29 | 30 | Once you have your `.env`, your db loaded, and your `box.json` dependencies installed, you can start your server. 31 | 32 | ```bash 33 | # Adobe 34 | run-script start:adobe 35 | run-script start:lucee 36 | ``` 37 | 38 | ## What can you do in the app? 39 | 40 | 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 41 | 42 | http://127.0.0.1:60146/ 43 | 44 | ### Hit API Endpoints 45 | 46 | - List rants: http://127.0.0.1:60146/api/v6/rants 47 | - Create Rants 48 | - Read Rants 49 | - Update Rants 50 | - Delete Rants 51 | 52 | ### View the Tests 53 | 54 | http://127.0.0.1:60146/tests/runner.cfm 55 | 56 | ### View API Docs 57 | 58 | http://127.0.0.1:60146/cbswagger 59 | 60 | #### What can you do with the API Docs? 61 | 62 | - Immport into Insomnia: https://insomnia.rest/ 63 | - Import into Postman: https://www.postman.com/ 64 | - Use with Swagger.io site: https://editor.swagger.io/ 65 | 66 | ### View Route Visualizer 67 | 68 | http://127.0.0.1:60146/route-visualizer 69 | 70 | ### View CBDebugger 71 | 72 | http://127.0.0.1:60146/cbdebugger 73 | -------------------------------------------------------------------------------- /resources/apidocs/_responses/rant.404.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Rant Not Found", 3 | "content": { 4 | "application/json": { 5 | "example": { 6 | "data": {}, 7 | "error": true, 8 | "pagination": {}, 9 | "messages": [ 10 | "Rant Not Found" 11 | ] 12 | }, 13 | "schema": { 14 | "type": "object", 15 | "properties": { 16 | "error": { 17 | "description": "Flag to indicate an error.", 18 | "type": "boolean" 19 | }, 20 | "messages": { 21 | "description": "An array of error messages.", 22 | "type": "array", 23 | "items": { 24 | "type": "string" 25 | } 26 | }, 27 | "data": { 28 | "description": "Empty response", 29 | "type": "string" 30 | } 31 | } 32 | } 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /resources/apidocs/_responses/user.404.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "User Not Found", 3 | "content": { 4 | "application/json": { 5 | "example": { 6 | "data": {}, 7 | "error": true, 8 | "pagination": {}, 9 | "messages": [ 10 | "User Not Found" 11 | ] 12 | }, 13 | "schema": { 14 | "type": "object", 15 | "properties": { 16 | "error": { 17 | "description": "Flag to indicate an error.", 18 | "type": "boolean" 19 | }, 20 | "messages": { 21 | "description": "An array of error messages.", 22 | "type": "array", 23 | "items": { 24 | "type": "string" 25 | } 26 | }, 27 | "data": { 28 | "description": "Empty response for 404", 29 | "type": "string" 30 | } 31 | } 32 | } 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /resources/apidocs/api-v6/Rants/create/example.200.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "rantID": "26" 4 | }, 5 | "error": false, 6 | "pagination": {}, 7 | "messages": [ 8 | "Rant created" 9 | ] 10 | } -------------------------------------------------------------------------------- /resources/apidocs/api-v6/Rants/create/example.400.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": {}, 3 | "error": true, 4 | "pagination": {}, 5 | "messages": [ 6 | "The 'BODY' value is required" 7 | ] 8 | } -------------------------------------------------------------------------------- /resources/apidocs/api-v6/Rants/create/requestBody.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Needed fields to create a Rant", 3 | "required": true, 4 | "content": { 5 | "application/json": { 6 | "schema": { 7 | "type": "object", 8 | "required": [ 9 | "body", 10 | "userID" 11 | ], 12 | "properties": { 13 | "body": { 14 | "description": "The body of the Rant.", 15 | "type": "string" 16 | }, 17 | "userID": { 18 | "description": "The id corresponding to the User who sent this Rant", 19 | "type": "integer" 20 | } 21 | }, 22 | "example": { 23 | "body": "I love to get up on my SoapBox and Rant away", 24 | "userID": 99 25 | } 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /resources/apidocs/api-v6/Rants/create/responses.json: -------------------------------------------------------------------------------- 1 | { 2 | "200": { 3 | "description": "Creates the Rant and shows the id for the newly created Rant", 4 | "content": { 5 | "application/json": { 6 | "example": { 7 | "$ref": "example.200.json" 8 | }, 9 | "schema": { 10 | "type": "object", 11 | "properties": { 12 | "error": { 13 | "description": "Flag to indicate an error.", 14 | "type": "boolean" 15 | }, 16 | "messages": { 17 | "description": "An array of error messages.", 18 | "type": "array", 19 | "items": { 20 | "type": "string" 21 | } 22 | }, 23 | "data": { 24 | "description": "The Data returned, a struct with a single key, rantID of the new Rant", 25 | "type": "object", 26 | "properties": { 27 | "rantID": { 28 | "description": "The ID of the newly created Rant.", 29 | "type": "integer" 30 | } 31 | } 32 | } 33 | 34 | } 35 | } 36 | } 37 | } 38 | }, 39 | "400": { 40 | "description": "Validation failed trying to create a Rant.", 41 | "content": { 42 | "application/json": { 43 | "example": { 44 | "$ref": "example.400.json" 45 | }, 46 | "schema": { 47 | "type": "object", 48 | "properties": { 49 | "error": { 50 | "description": "Flag to indicate an error.", 51 | "type": "boolean" 52 | }, 53 | "messages": { 54 | "description": "An array of error messages.", 55 | "type": "array", 56 | "items": { 57 | "type": "string" 58 | } 59 | }, 60 | "data": { 61 | "description": "Empty response for 400", 62 | "type": "string" 63 | } 64 | } 65 | } 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /resources/apidocs/api-v6/Rants/delete/example.200.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": {}, 3 | "error": false, 4 | "pagination": {}, 5 | "messages": [ 6 | "Rant deleted" 7 | ] 8 | } -------------------------------------------------------------------------------- /resources/apidocs/api-v6/Rants/delete/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "parameters": [ 3 | { 4 | "name": "rantID", 5 | "description": "The identifier for the Rant to Delete.", 6 | "schema": { 7 | "type": "integer" 8 | }, 9 | "required": true, 10 | "in": "path" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /resources/apidocs/api-v6/Rants/delete/responses.json: -------------------------------------------------------------------------------- 1 | { 2 | "200": { 3 | "description": "Deletes the Rant and a message stating it was done", 4 | "content": { 5 | "application/json": { 6 | "example": { 7 | "$ref": "example.200.json" 8 | }, 9 | "schema": { 10 | "type": "object", 11 | "properties": { 12 | "error": { 13 | "description": "Flag to indicate an error.", 14 | "type": "boolean" 15 | }, 16 | "messages": { 17 | "description": "An array of error messages.", 18 | "type": "array", 19 | "items": { 20 | "type": "string" 21 | } 22 | }, 23 | "data": { 24 | "description": "Empty string", 25 | "type": "string" 26 | } 27 | } 28 | } 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /resources/apidocs/api-v6/Rants/index/example.200.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "userId": 2, 5 | "createdDate": "May, 04 2020 21:15:10 -0700", 6 | "id": 16, 7 | "modifiedDate": "May, 04 2020 21:15:10 -0700", 8 | "body": "Another rant, where's my soapbox" 9 | }, 10 | { 11 | "userId": 2, 12 | "createdDate": "May, 04 2020 21:09:13 -0700", 13 | "id": 15, 14 | "modifiedDate": "May, 04 2020 21:09:13 -0700", 15 | "body": "CommandBox, ColdBox, TestBox, all the Boxes!!!" 16 | } 17 | ], 18 | "error": false, 19 | "pagination": {}, 20 | "messages": [] 21 | } -------------------------------------------------------------------------------- /resources/apidocs/api-v6/Rants/index/responses.json: -------------------------------------------------------------------------------- 1 | { 2 | "200": { 3 | "description": "A list of Agents", 4 | "content": { 5 | "application/json": { 6 | "schema": { 7 | "type": "object", 8 | "properties": { 9 | "error": { 10 | "description": "Flag to indicate an error.", 11 | "type": "boolean" 12 | }, 13 | "messages": { 14 | "description": "An array of error messages.", 15 | "type": "array", 16 | "items": { 17 | "type": "string" 18 | } 19 | }, 20 | "data": { 21 | "description": "An array of Rants.", 22 | "type": "array", 23 | "items": { 24 | "type": "object", 25 | "properties": { 26 | "id": { 27 | "description": "The Rant ID", 28 | "type": "integer" 29 | }, 30 | "body": { 31 | "description": "The body of the Rant", 32 | "type": "string" 33 | }, 34 | "userID": { 35 | "description": "The ID of the creating User", 36 | "type": "integer" 37 | }, 38 | "createdDate": { 39 | "description": "The Date and Time the Rant was Created", 40 | "type": "string" 41 | }, 42 | "modifiedDate": { 43 | "description": "The Date and Time the Rant was Modified", 44 | "type": "string" 45 | } 46 | } 47 | } 48 | } 49 | } 50 | }, 51 | "example": { 52 | "$ref": "example.200.json" 53 | } 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /resources/apidocs/api-v6/Rants/show/example.200.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "userId": 2, 4 | "createdDate": "May, 04 2020 20:59:44 -0700", 5 | "id": 12, 6 | "modifiedDate": "May, 04 2020 20:59:44 -0700", 7 | "body": "Another rant, where's my soapbox2" 8 | }, 9 | "error": false, 10 | "pagination": {}, 11 | "messages": [] 12 | } -------------------------------------------------------------------------------- /resources/apidocs/api-v6/Rants/show/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "parameters": [ 3 | { 4 | "name": "rantID", 5 | "description": "The identifier for the Rant to Show.", 6 | "schema": { 7 | "type": "integer" 8 | }, 9 | "required": true, 10 | "in": "path" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /resources/apidocs/api-v6/Rants/show/responses.json: -------------------------------------------------------------------------------- 1 | { 2 | "200": { 3 | "description": "Returns the requested Rant", 4 | "content": { 5 | "application/json": { 6 | "example": { 7 | "$ref": "example.200.json" 8 | }, 9 | "schema": { 10 | "type": "object", 11 | "properties": { 12 | "error": { 13 | "description": "Flag to indicate an error.", 14 | "type": "boolean" 15 | }, 16 | "messages": { 17 | "description": "An array of error messages.", 18 | "type": "array", 19 | "items": { 20 | "type": "string" 21 | } 22 | }, 23 | "data": { 24 | "description": "The Rant returned", 25 | "type": "object", 26 | "properties": { 27 | "id": { 28 | "description": "The Rant ID", 29 | "type": "integer" 30 | }, 31 | "body": { 32 | "description": "The body of the Rant", 33 | "type": "string" 34 | }, 35 | "userID": { 36 | "description": "The ID of the creating User", 37 | "type": "integer" 38 | }, 39 | "createdDate": { 40 | "description": "The Date and Time the Rant was Created", 41 | "type": "string" 42 | }, 43 | "modifiedDate": { 44 | "description": "The Date and Time the Rant was Modified", 45 | "type": "string" 46 | } 47 | } 48 | } 49 | 50 | } 51 | } 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /resources/apidocs/api-v6/Rants/update/example.200.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": {}, 3 | "error": false, 4 | "pagination": {}, 5 | "messages": [ 6 | "Rant Updated" 7 | ] 8 | } -------------------------------------------------------------------------------- /resources/apidocs/api-v6/Rants/update/example.400.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": {}, 3 | "error": true, 4 | "pagination": {}, 5 | "messages": [ 6 | "The 'BODY' value is required" 7 | ] 8 | } -------------------------------------------------------------------------------- /resources/apidocs/api-v6/Rants/update/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "parameters": [ 3 | { 4 | "name": "rantID", 5 | "description": "The identifier for the Rant to Update.", 6 | "schema": { 7 | "type": "integer" 8 | }, 9 | "required": true, 10 | "in": "path" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /resources/apidocs/api-v6/Rants/update/requestBody.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Needed fields to Update a Rant", 3 | "required": true, 4 | "content": { 5 | "application/json": { 6 | "schema": { 7 | "type": "object", 8 | "required": [ 9 | "body", 10 | "userID" 11 | ], 12 | "properties": { 13 | "body": { 14 | "description": "The body of the Rant.", 15 | "type": "string" 16 | }, 17 | "userID": { 18 | "description": "The id corresponding to the User who sent this Rant", 19 | "type": "integer" 20 | } 21 | }, 22 | "example": { 23 | "body": "I love to get up on my SoapBox and Rant away", 24 | "userID": 99 25 | } 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /resources/apidocs/api-v6/Rants/update/responses.json: -------------------------------------------------------------------------------- 1 | { 2 | "200": { 3 | "description": "Updates the Rant", 4 | "content": { 5 | "application/json": { 6 | "example": { 7 | "$ref": "example.200.json" 8 | }, 9 | "schema": { 10 | "type": "object", 11 | "properties": { 12 | "error": { 13 | "description": "Flag to indicate an error.", 14 | "type": "boolean" 15 | }, 16 | "messages": { 17 | "description": "An array of error messages.", 18 | "type": "array", 19 | "items": { 20 | "type": "string" 21 | } 22 | }, 23 | "data": { 24 | "description": "Empty string for Rant Update", 25 | "type": "string" 26 | } 27 | } 28 | } 29 | } 30 | } 31 | }, 32 | "400": { 33 | "description": "Validation failed trying to update the Rant.", 34 | "content": { 35 | "application/json": { 36 | "example": { 37 | "$ref": "example.400.json" 38 | }, 39 | "schema": { 40 | "type": "object", 41 | "properties": { 42 | "error": { 43 | "description": "Flag to indicate an error.", 44 | "type": "boolean" 45 | }, 46 | "messages": { 47 | "description": "An array of error messages.", 48 | "type": "array", 49 | "items": { 50 | "type": "string" 51 | } 52 | }, 53 | "data": { 54 | "description": "Empty response for 400", 55 | "type": "string" 56 | } 57 | } 58 | } 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /resources/database/migrations/2020_05_15_183916_users.cfc: -------------------------------------------------------------------------------- 1 | component { 2 | 3 | function up( schema, query ) { 4 | schema.create( "users", ( table ) => { 5 | table.string( "id" ).primaryKey(); 6 | table.string( "username" ).unique(); 7 | table.string( "email" ).unique(); 8 | table.string( "password" ); 9 | table.datetime( "createdDate" ).withCurrent(); 10 | table.datetime( "updatedDate" ).withCurrent(); 11 | } ); 12 | } 13 | 14 | function down( schema, query ) { 15 | schema.drop( "users" ); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /resources/database/migrations/2020_05_15_183939_rants.cfc: -------------------------------------------------------------------------------- 1 | component { 2 | 3 | function up( schema, query ) { 4 | schema.create( "rants", ( table ) => { 5 | table.string( "id" ).primaryKey() 6 | table.text( "body" ) 7 | table.datetime( "createdDate" ).withCurrent() 8 | table.datetime( "updatedDate" ).withCurrent() 9 | table.string( "userId" ); 10 | table.foreignKey( "userId" ).references( "id" ).onTable( "users" ); 11 | } ); 12 | } 13 | 14 | function down( schema, query ) { 15 | schema.drop( "rants" ); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /resources/database/migrations/2020_05_15_184033_seedrants.cfc: -------------------------------------------------------------------------------- 1 | component { 2 | 3 | messages = [ 4 | 'Another rant', 5 | "This is the most amazing post in my life", 6 | "I love kittens", 7 | "I love espresso", 8 | "I love soccer!", 9 | "Why is this here!", 10 | "Captain America is the best superhero!", 11 | 'Testing test test', 12 | 'Scott likes me preso', 13 | 'Scott seems to like my preso', 14 | 'What are you talking about!', 15 | "This post is not really good, it sucked!", 16 | "Why are you doing this to me!", 17 | "Please please please delete!" 18 | ]; 19 | 20 | users = [ 21 | { 22 | id : '#createUUID()#' , 23 | username : 'gpickin', 24 | email : 'gavin@ortussolutions.com', 25 | password : '$2a$12$JKiBJZF352Tfm/c3PpeslOBKRAwtXlwczMPKeUV1raD0d1cwh5B5.' 26 | }, 27 | { 28 | id : '#createUUID()#' , 29 | username : 'luis', 30 | email : 'lmajano@ortussolutions.com', 31 | password : '$2a$12$FE2J7ZLWaI2rSqejAu/84uLy7qlSufQsDsSE1lNNKyA05GG30gr8C' 32 | }, 33 | { 34 | id : '#createUUID()#' , 35 | username : 'brad', 36 | email : 'brad@ortussolutions.com', 37 | password : '$2a$12$Vbb4dYywI5X.1qKEV2mDzeOTZk3iHIDfEtz80SoMT0KkFWTkb.PB6' 38 | }, 39 | { 40 | id : '#createUUID()#' , 41 | username : 'javier', 42 | email : 'jquintero@ortussolutions.com', 43 | password : '$2a$12$UIEOglSflvGUbn5sHeBZ1.sAlaoBI4rpNOCIk2vF8R2KKz.ihP9/W' 44 | }, 45 | { 46 | id : '#createUUID()#' , 47 | username : 'scott', 48 | email : 'scott@scott.com', 49 | password : '$2a$12$OjIpxecG9AlZTgVGV1jsvOegTwbqgJ29PlUkfomGsK/6hsVicsRW.' 50 | }, 51 | { 52 | id : '#createUUID()#' , 53 | username : 'mike', 54 | email : 'mikep@netxn.com', 55 | password : '$2a$12$WWUwFEAoDGx.vB0jE54xser1myMUSwUMYo/aNn0cSGa8l6DQe67Q2' 56 | } 57 | ]; 58 | 59 | function up( schema, query ) { 60 | // insert users 61 | query.newQuery().from( "users" ).insert( users ); 62 | 63 | // insert random rants 64 | var rants = []; 65 | for( var x = 1; x lte 20; x++ ){ 66 | rants.append( { 67 | "id" : createUUID(), 68 | "body" : messages[ randRange( 1, messages.len() ) ], 69 | "userId" : users[ randRange( 1, users.len() ) ].id 70 | } ); 71 | } 72 | query.newQuery().from( "rants" ).insert( rants ); 73 | } 74 | 75 | function down( schema, query ) { 76 | queryExecute( "truncate rants" ); 77 | queryExecute( "truncate users" ); 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /server-adobe.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"fluentapi-adobe", 3 | "app":{ 4 | "serverHomeDirectory":".engine/adobe2023", 5 | "cfengine":"adobe@2023" 6 | }, 7 | "web":{ 8 | "http":{ 9 | "port":"60146" 10 | }, 11 | "rewrites":{ 12 | "enable":"true" 13 | } 14 | }, 15 | "cfconfig":{ 16 | "file":".cfconfig.json" 17 | }, 18 | "scripts":{ 19 | "onServerInstall":"cfpm install mysql,debugger,chart" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /server-boxlang.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"fluentapi-boxlang", 3 | "app":{ 4 | "serverHomeDirectory":".engine/boxlang", 5 | "cfengine":"boxlang@be" 6 | }, 7 | "web":{ 8 | "http":{ 9 | "port":"60147" 10 | }, 11 | "rewrites":{ 12 | "enable":"true" 13 | } 14 | }, 15 | "JVM":{ 16 | "javaVersion":"openjdk21_jdk", 17 | "args":"-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8889" 18 | }, 19 | "cfconfig":{ 20 | "file":".cfconfig.json" 21 | }, 22 | "env":{ 23 | "BOXLANG_DEBUG":true 24 | }, 25 | "scripts":{ 26 | "onServerInitialInstall":"install bx-compat-cfml,bx-unsafe-evaluate,bx-mysql" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/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 | 8 | // APPLICATION CFC PROPERTIES 9 | this.name = "ColdBoxTestingSuite"; 10 | this.sessionManagement = true; 11 | this.setClientCookies = true; 12 | this.sessionTimeout = createTimespan( 0, 0, 15, 0 ); 13 | this.applicationTimeout = createTimespan( 0, 0, 15, 0 ); 14 | this.whiteSpaceManagement = "smart"; 15 | this.datasource = "fluentAPI"; 16 | /** 17 | * -------------------------------------------------------------------------- 18 | * Location Mappings 19 | * -------------------------------------------------------------------------- 20 | * - cbApp : Quick reference to root application 21 | * - coldbox : Where ColdBox library is installed 22 | * - testbox : Where TestBox is installed 23 | */ 24 | // Create testing mapping 25 | this.mappings[ "/tests" ] = getDirectoryFromPath( getCurrentTemplatePath() ); 26 | // The root application mapping 27 | rootPath = reReplaceNoCase( this.mappings[ "/tests" ], "tests(\\|/)", "" ); 28 | this.mappings[ "/root" ] = this.mappings[ "/cbapp" ] = rootPath; 29 | this.mappings[ "/coldbox" ] = rootPath & "coldbox"; 30 | this.mappings[ "/testbox" ] = rootPath & "testbox"; 31 | 32 | /** 33 | * Fires on every test request. It builds a Virtual ColdBox application for you 34 | * 35 | * @targetPage The requested page 36 | */ 37 | public boolean function onRequestStart( targetPage ){ 38 | // Set a high timeout for long running tests 39 | setting requestTimeout ="9999"; 40 | // New ColdBox Virtual Application Starter 41 | request.coldBoxVirtualApp= new coldbox.system.testing.VirtualApp( appMapping = "/root" ); 42 | 43 | // If hitting the runner or specs, prep our virtual app 44 | if ( getBaseTemplatePath().replace( expandPath( "/tests" ), "" ).reFindNoCase( "(runner|specs)" ) ) { 45 | request.coldBoxVirtualApp.startup(); 46 | } 47 | 48 | // Reload for fresh results 49 | if ( structKeyExists( url, "fwreinit" ) ) { 50 | if ( structKeyExists( server, "lucee" ) ) { 51 | pagePoolClear(); 52 | } 53 | // ormReload(); 54 | request.coldBoxVirtualApp.restart(); 55 | } 56 | 57 | return true; 58 | } 59 | 60 | /** 61 | * Fires when the testing requests end and the ColdBox application is shutdown 62 | */ 63 | public void function onRequestEnd( required targetPage ){ 64 | request.coldBoxVirtualApp.shutdown(); 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /tests/index.cfm: -------------------------------------------------------------------------------- 1 | 2 | // No cf debugging 3 | cfsetting( showdebugoutput="false" ); 4 | // GLOBAL VARIABLES 5 | ASSETS_DIR = expandPath( "/testbox/system/reports/assets" ); 6 | TESTBOX_VERSION = new testBox.system.TestBox().getVersion(); 7 | // TEST LOCATIONS -> UPDATE AS YOU SEE FIT 8 | rootMapping = "/tests"; 9 | 10 | // Local Variables 11 | rootPath = expandPath( rootMapping ); 12 | targetPath = rootPath; 13 | 14 | // Incoming Navigation 15 | param name="url.path" default=""; 16 | if( len( url.path ) ){ 17 | targetPath = getCanonicalPath( rootpath & "/" & url.path ); 18 | // Avoid traversals, reset to root 19 | if( !findNoCase( rootpath, targetPath ) ){ 20 | targetPath = rootpath; 21 | } 22 | } 23 | 24 | // Get the actual execution path 25 | executePath = rootMapping & ( len( url.path ) ? "/#url.path#" : "/" ); 26 | // Execute an incoming path 27 | if( !isNull( url.action ) ){ 28 | if( directoryExists( targetPath ) ){ 29 | writeOutput( "#new testbox.system.TestBox( directory=executePath ).run()#" ); 30 | } else { 31 | writeOutput( "

Invalid Directory: #encodeForHTML( targetPath )#

" ); 32 | } 33 | abort; 34 | } 35 | 36 | // Get the tests to navigate 37 | qResults = directoryList( targetPath, false, "query", "", "name" ); 38 | 39 | // Calculate the back navigation path 40 | if( len( url.path ) ){ 41 | backPath = url.path.listToArray( "/\" ); 42 | backPath.pop(); 43 | backPath = backPath.toList( "/" ); 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_VERSION# 71 |
72 | 76 | 81 | 82 |
83 |
84 | 85 | 86 |
87 |
88 |

Availble Test Runners:

89 |

90 | Below is a listing of the runners matching the "runner*.(cfm|bxm)" pattern. 91 |

92 | 93 | 94 | 95 |

No runners found in this directory

96 | 97 | 98 | 102 | class="btn btn-success btn-sm my-1 mx-1" 103 | 104 | class="btn btn-info btn-sm my-1 mx-1" 105 |
106 | > 107 | #runners.name# 108 | 109 | 110 | 111 |
112 |
113 | 114 | 115 |
116 |
117 |
118 | 119 |

TestBox Test Browser:

120 |

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

124 | 125 |
126 | #targetPath.replace( rootPath, "" )# 127 | 128 | 129 | 130 | 131 | 132 | 133 |
134 |
135 |
136 | 137 | 138 | 139 | 146 | 147 | 148 | 149 | 150 | 154 | &##x271A; #qResults.name# 155 | 156 |
157 | 158 | 163 | #qResults.name# 164 | 165 |
166 | 169 | 171 | data-bx="true" 172 | class="btn btn-success btn-sm my-1" 173 | 174 | data-bx="false" 175 | class="btn btn-info btn-sm my-1" 176 |
177 | href="#executePath & "/" & qResults.name#?method=runRemote" 178 | target="_blank" 179 | > 180 | #qResults.name# 181 | 182 |
183 | 184 | 185 |
186 |
187 |
188 |
189 |
190 |
191 | 192 | 193 | 194 |
195 | -------------------------------------------------------------------------------- /tests/resources/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lmajano/modern-functional-fluent-cfml-rest/920c6129fb65cdaace93c967ccde52e90464d7ba/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 | /** 9 | * executes before all suites+specs in the run() method 10 | */ 11 | function beforeAll(){ 12 | super.beforeAll(); 13 | 14 | addMatchers( { 15 | toHaveLengthGT : function( expectation, args = {}, lengthTest=variables.lengthTest ) { 16 | args[ "operator" ] = "GT"; 17 | return arguments.lengthTest( expectation, args ); 18 | }, 19 | 20 | toHaveLengthGTE : function( expectation, args = {}, lengthTest=variables.lengthTest ) { 21 | args[ "operator" ] = "GTE"; 22 | return arguments.lengthTest( expectation, args ); 23 | }, 24 | 25 | toHaveLengthLT : function( expectation, args = {}, lengthTest=variables.lengthTest ) { 26 | args[ "operator" ] = "LT"; 27 | return arguments.lengthTest( expectation, args ); 28 | }, 29 | 30 | toHaveLengthLTE : function( expectation, args = {}, lengthTest=variables.lengthTest ) { 31 | args[ "operator" ] = "LTE"; 32 | return arguments.lengthTest( expectation, args ); 33 | } 34 | } ); 35 | } 36 | 37 | /** 38 | * A length test 39 | */ 40 | private function lengthTest( expectation, args = {} ) { 41 | // handle both positional and named arguments 42 | param args.value = ""; 43 | if ( structKeyExists( args, 1 ) ) { 44 | args.value = args[ 1 ]; 45 | } 46 | 47 | param args.message = ""; 48 | if ( structKeyExists( args, 2 ) ) { 49 | args.message = args[ 2 ]; 50 | } 51 | 52 | param args.operator = "GT"; 53 | if ( structKeyExists( args, 3 ) ) { 54 | args.value = args[ 3 ]; 55 | } 56 | 57 | if ( !isNumeric( args.value )) { 58 | expectation.message = "The value you are testing must be a valid number"; 59 | return false; 60 | } 61 | try{ 62 | var length = 0; 63 | if ( isSimpleValue( expectation.actual ) ) { 64 | length = len( expectation.actual ); 65 | } 66 | if ( isArray( expectation.actual ) ) { 67 | length = arrayLen( expectation.actual ); 68 | } 69 | if ( isStruct( expectation.actual ) ) { 70 | length = structCount( expectation.actual ); 71 | } 72 | if ( isQuery( expectation.actual ) ) { 73 | length = expectation.actual.recordcount; 74 | } 75 | 76 | } catch ( any e ){ 77 | expectation.message = "The length of the Item could not be found"; 78 | return false; 79 | } 80 | 81 | if( args.operator == "GT" && length <= args.value ){ 82 | expectation.message = "The length of the item was #length# - that is not GT #args.value#"; 83 | debug( expectation.actual ); 84 | return false; 85 | } else if( args.operator == "GTE" && length < args.value ){ 86 | expectation.message = "The length of the item was #length# - that is not GTE #args.value#"; 87 | debug( expectation.actual ); 88 | return false; 89 | } else if( args.operator == "LT" && length >= args.value ){ 90 | expectation.message = "The length of the item was #length# - that is not LT #args.value#"; 91 | debug( expectation.actual ); 92 | return false; 93 | } else if( args.operator == "LTE" && length > args.value ){ 94 | expectation.message = "The length of the item was #length# - that is not LTE #args.value#"; 95 | debug( expectation.actual ); 96 | return false; 97 | } 98 | 99 | return true; 100 | }; 101 | 102 | /** 103 | * executes after all suites+specs in the run() method 104 | */ 105 | function afterAll(){ 106 | super.afterAll(); 107 | } 108 | 109 | /** 110 | * Rollback all testing, called by TestBox for me 111 | * 112 | * @spec The spec in test 113 | * @suite The suite in test 114 | */ 115 | function withRollback( spec, suite ) aroundEach { 116 | transaction{ 117 | try{ 118 | return arguments.spec.body(); 119 | } catch( any e ){ 120 | rethrow; 121 | } finally{ 122 | transaction action="rollback"; 123 | } 124 | } 125 | } 126 | 127 | } 128 | -------------------------------------------------------------------------------- /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 extends="coldbox.system.testing.BaseTestCase" { 18 | 19 | /*********************************** LIFE CYCLE Methods ***********************************/ 20 | 21 | function beforeAll(){ 22 | super.beforeAll(); 23 | // do your own stuff here 24 | } 25 | 26 | function afterAll(){ 27 | // do your own stuff here 28 | super.afterAll(); 29 | } 30 | 31 | /*********************************** BDD SUITES ***********************************/ 32 | 33 | function run(){ 34 | describe( "My RESTFUl Service v1", function(){ 35 | beforeEach( function( currentSpec ){ 36 | // Setup as a new ColdBox request, VERY IMPORTANT. ELSE EVERYTHING LOOKS LIKE THE SAME REQUEST. 37 | setup(); 38 | } ); 39 | 40 | it( "can handle invalid HTTP Calls", function(){ 41 | var event = execute( 42 | event = "v1:echo.onInvalidHTTPMethod", 43 | renderResults = true, 44 | eventArguments = { faultAction : "test" } 45 | ); 46 | var response = event.getPrivateValue( "response" ); 47 | expect( response.getError() ).toBeTrue(); 48 | expect( response.getStatusCode() ).toBe( 405 ); 49 | } ); 50 | 51 | it( "can handle global exceptions", function(){ 52 | var event = execute( 53 | event = "v1:echo.onError", 54 | renderResults = true, 55 | eventArguments = { 56 | exception : { 57 | message : "unit test", 58 | detail : "unit test", 59 | stacktrace : "" 60 | } 61 | } 62 | ); 63 | 64 | var response = event.getPrivateValue( "response" ); 65 | expect( response.getError() ).toBeTrue(); 66 | expect( response.getStatusCode() ).toBe( 500 ); 67 | } ); 68 | 69 | it( "can handle an echo", function(){ 70 | var event = this.request( "/api/v1/echo/index" ); 71 | var response = event.getPrivateValue( "response" ); 72 | expect( response.getError() ).toBeFalse(); 73 | expect( response.getData() ).toBe( "Welcome to my ColdBox RESTFul Service V1" ); 74 | } ); 75 | 76 | it( "can handle missing actions", function(){ 77 | var event = this.request( "/api/v1/echo/bogus" ); 78 | var response = event.getPrivateValue( "response" ); 79 | expect( response.getError() ).tobeTrue(); 80 | expect( response.getStatusCode() ).toBe( 404 ); 81 | } ); 82 | } ); 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /tests/specs/integration/api-v3/RantsTest.cfc: -------------------------------------------------------------------------------- 1 | component extends="tests.resources.BaseTest" { 2 | 3 | function run(){ 4 | describe( "Rants V3 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 | story( "Get a list of Rants", function(){ 11 | given( "I make a get call to /api/v3/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/v3/rants" ); 15 | var returnedJSON = event.getRenderData().data; 16 | expect( returnedJSON.error ).toBeFalse(); 17 | expect( returnedJSON.data ).toBeArray(); 18 | expect( returnedJSON.data ).toHaveLengthGTE( 1 ); 19 | } ); 20 | } ); 21 | } ); 22 | } ); 23 | 24 | story( "Get an individual Rant", function(){ 25 | given( "I make a get call to /api/v3/rants/:rantID", function(){ 26 | when( "I pass an invalid rantID", function(){ 27 | then( "I will get a 400 error", function(){ 28 | var rantID = "x" 29 | var event = get( "/api/v3/rants/#rantID#" ); 30 | var returnedJSON = event.getRenderData().data; 31 | expect( returnedJSON.error ).toBeTrue(); 32 | expect( event ).toHaveStatus( 400 ); 33 | expect( returnedJSON.messages ).toHaveLengthGTE( 1 ); 34 | } ); 35 | } ); 36 | 37 | when( "I pass a valid but non existing rantID", function(){ 38 | then( "I will get a 404 error", function(){ 39 | var rantID = createUUID(); 40 | var event = get( "/api/v3/rants/#rantID#" ); 41 | var returnedJSON = event.getRenderData().data; 42 | expect( returnedJSON.error ).toBeTrue(); 43 | expect( event ).toHaveStatus( 404 ); 44 | expect( returnedJSON.messages ).toHaveLengthGTE( 1 ); 45 | expect( returnedJSON.messages[ 1 ] ).toInclude( "rant not found" ); 46 | } ); 47 | } ); 48 | 49 | when( "I pass a valid and existing rantID", function(){ 50 | then( "I will get a single Rant returned", function(){ 51 | var testRantId = queryExecute( "select id from rants limit 1" ).id; 52 | var event = get( "/api/v3/rants/#testRantId#" ); 53 | var returnedJSON = event.getRenderData().data; 54 | expect( returnedJSON.error ).toBeFalse(); 55 | expect( event ).toHaveStatus( 200 ); 56 | expect( returnedJSON.data ).toBeStruct(); 57 | expect( returnedJSON.data ).toHaveKey( "ID" ); 58 | expect( returnedJSON.data.id ).toBe( testRantId ); 59 | expect( returnedJSON.messages ).toHaveLength( 0 ); 60 | } ); 61 | } ); 62 | } ); 63 | } ); 64 | 65 | story( "Create a Rant", function(){ 66 | given( "I make a post call to /api/v3/rants", function(){ 67 | when( "Using a get method", function(){ 68 | then( "I will hit the index action instead of the create action", function(){ 69 | var event = get( "/api/v3/rants" ); 70 | expect( event.getCurrentAction() ).toBe( 71 | "index", 72 | "Expected to hit index action not [#event.getCurrentAction()#] action" 73 | ); 74 | } ); 75 | } ); 76 | 77 | when( "Including no userID param", function(){ 78 | then( "I will get a 400 error", function(){ 79 | var event = post( "/api/v3/rants", {} ); 80 | var returnedJSON = event.getRenderData().data; 81 | expect( returnedJSON.error ).toBeTrue(); 82 | expect( event ).toHaveStatus( 400 ); 83 | expect( returnedJSON.messages ).toHaveLengthGTE( 1 ); 84 | } ); 85 | } ); 86 | 87 | when( "Including an empty userID param", function(){ 88 | then( "I will get a 400 error", function(){ 89 | var event = post( "/api/v3/rants", { "userID" : "" } ); 90 | var returnedJSON = event.getRenderData().data; 91 | expect( returnedJSON.error ).toBeTrue(); 92 | expect( event ).toHaveStatus( 400 ); 93 | expect( returnedJSON.messages ).toHaveLengthGTE( 1 ); 94 | } ); 95 | } ); 96 | 97 | when( "Including a non uuid userID param", function(){ 98 | then( "I will get a 400 error", function(){ 99 | var event = post( "/api/v3/rants", { "userID" : "abc" } ); 100 | var returnedJSON = event.getRenderData().data; 101 | expect( returnedJSON.error ).toBeTrue(); 102 | expect( event ).toHaveStatus( 400 ); 103 | expect( returnedJSON.messages ).toHaveLengthGTE( 1 ); 104 | } ); 105 | } ); 106 | 107 | when( "Including no body param", function(){ 108 | then( "I will get a 400 error", function(){ 109 | var event = post( "/api/v3/rants", { "userID" : "5" } ); 110 | var returnedJSON = event.getRenderData().data; 111 | expect( returnedJSON.error ).toBeTrue(); 112 | expect( event ).toHaveStatus( 400 ); 113 | expect( returnedJSON.messages ).toHaveLengthGTE( 1 ); 114 | } ); 115 | } ); 116 | 117 | when( "Including an empty body param", function(){ 118 | then( "I will get a 400 error", function(){ 119 | var event = post( "/api/v3/rants", { "userID" : "5", "body" : "" } ); 120 | var returnedJSON = event.getRenderData().data; 121 | expect( returnedJSON.error ).toBeTrue(); 122 | expect( event ).toHaveStatus( 400 ); 123 | expect( returnedJSON.messages ).toHaveLengthGTE( 1 ); 124 | } ); 125 | } ); 126 | 127 | when( "Including valid userID for a non existing User", function(){ 128 | then( "I will get a 404 error", function(){ 129 | var event = post( "/api/v3/rants", { "body" : "xsxswxws", "userID" : createUUID() } ); 130 | var returnedJSON = event.getRenderData().data; 131 | debug( returnedJSON ); 132 | expect( returnedJSON.error ).toBeTrue(); 133 | expect( event ).toHaveStatus( 404 ); 134 | expect( returnedJSON.messages ).toHaveLengthGTE( 1 ); 135 | expect( returnedJSON.messages[ 1 ] ).toInclude( "user not found" ); 136 | } ); 137 | } ); 138 | 139 | when( "I pass a valid body and userID", function(){ 140 | then( "I will get a successful query result with a generatedKey", function(){ 141 | var testUserId = queryExecute( "select id from users limit 1" ).id; 142 | var event = post( "/api/v3/rants", { "body" : "xsxswxws", "userID" : testUserId } ); 143 | var returnedJSON = event.getRenderData().data; 144 | expect( returnedJSON.error ).toBeFalse(); 145 | expect( event.getStatusCode() ).toBe( 200 ); 146 | expect( returnedJSON.data ).toBeStruct(); 147 | expect( returnedJSON.data ).toHaveKey( "rantId" ); 148 | expect( returnedJSON.messages ).toHaveLengthGTE( 1 ); 149 | expect( returnedJSON.messages[ 1 ] ).toBe( "Rant Created" ); 150 | } ); 151 | } ); 152 | } ); 153 | } ); 154 | 155 | story( "Update a Rant", function(){ 156 | given( "I make a get call to /api/v3/rants/:rantID", function(){ 157 | when( "Using a get method", function(){ 158 | then( "I will hit the show action instead of the update action", function(){ 159 | var rantID = "1"; 160 | var event = get( "/api/v3/rants/#rantID#" ); 161 | expect( event.getCurrentAction() ).toBe( 162 | "show", 163 | "I expect to hit show action instead of the update action due to the VERB, but I actually hit [#event.getCurrentAction()#]" 164 | ); 165 | } ); 166 | } ); 167 | 168 | when( "Using a post method", function(){ 169 | then( "I will hit the show action instead of the update action", function(){ 170 | var rantID = "1"; 171 | var event = post( "/api/v3/rants/#rantID#" ); 172 | expect( event.getCurrentAction() ).toBe( 173 | "onInvalidHTTPMethod", 174 | "I expect to hit onInvalidHTTPMethod action instead of the update action due to the VERB, but I actually hit [#event.getCurrentAction()#]" 175 | ); 176 | } ); 177 | } ); 178 | 179 | when( "Including no userID param", function(){ 180 | then( "I will get a 400 error", function(){ 181 | var rantID = createUUID(); 182 | var event = put( "/api/v3/rants/#rantID#", {} ); 183 | var returnedJSON = event.getRenderData().data; 184 | debug( returnedJSON ); 185 | expect( returnedJSON.error ).toBeTrue(); 186 | expect( event ).toHaveStatus( 400 ); 187 | expect( returnedJSON.messages ).toHaveLengthGTE( 1 ); 188 | } ); 189 | } ); 190 | 191 | when( "Including an empty userID param", function(){ 192 | then( "I will get a 400 error", function(){ 193 | var rantID = createUUID(); 194 | var event = put( "/api/v3/rants/#rantID#", { "userID" : "" } ); 195 | var returnedJSON = event.getRenderData().data; 196 | expect( returnedJSON.error ).toBeTrue(); 197 | expect( event ).toHaveStatus( 400 ); 198 | expect( returnedJSON.messages ).toHaveLengthGTE( 1 ); 199 | } ); 200 | } ); 201 | 202 | when( "Including a non uuid userID param", function(){ 203 | then( "I will get a 400 error", function(){ 204 | var rantID = createUUID(); 205 | var event = put( "/api/v3/rants/#rantID#", { "userID" : "abc" } ); 206 | var returnedJSON = event.getRenderData().data; 207 | expect( returnedJSON.error ).toBeTrue(); 208 | expect( event ).toHaveStatus( 400 ); 209 | expect( returnedJSON.messages ).toHaveLengthGTE( 1 ); 210 | } ); 211 | } ); 212 | 213 | when( "Including no body param", function(){ 214 | then( "I will get a 400 error", function(){ 215 | var rantID = createUUID(); 216 | var event = put( "/api/v3/rants/#rantID#", { "userID" : createUUID() } ); 217 | var returnedJSON = event.getRenderData().data; 218 | expect( returnedJSON.error ).toBeTrue(); 219 | expect( event ).toHaveStatus( 400 ); 220 | expect( returnedJSON.messages ).toHaveLengthGTE( 1 ); 221 | } ); 222 | } ); 223 | 224 | when( "Including an empty body param", function(){ 225 | then( "I will get a 400 error", function(){ 226 | var rantID = createUUID(); 227 | var event = put( "/api/v3/rants/#rantID#", { "userID" : createUUID(), "body" : "" } ); 228 | var returnedJSON = event.getRenderData().data; 229 | expect( returnedJSON.error ).toBeTrue(); 230 | expect( event ).toHaveStatus( 400 ); 231 | expect( returnedJSON.messages ).toHaveLengthGTE( 1 ); 232 | } ); 233 | } ); 234 | 235 | when( "Including a non uuid rantID param", function(){ 236 | then( "I will get a 400 error", function(){ 237 | var rantID = "abc"; 238 | var event = put( 239 | "/api/v3/rants/#rantID#", 240 | { "userID" : createUUID(), "body" : "abc" } 241 | ); 242 | var returnedJSON = event.getRenderData().data; 243 | expect( returnedJSON.error ).toBeTrue(); 244 | expect( event ).toHaveStatus( 400 ); 245 | expect( returnedJSON.messages ).toHaveLengthGTE( 1 ); 246 | } ); 247 | } ); 248 | 249 | when( "Including valid userID for a non existing User", function(){ 250 | then( "I will get a 404 error", function(){ 251 | var testRantId = queryExecute( "select id from rants limit 1" ).id; 252 | var event = put( 253 | "/api/v3/rants/#testRantId#", 254 | { "body" : "xsxswxws", "userID" : createUUID() } 255 | ); 256 | var returnedJSON = event.getRenderData().data; 257 | expect( returnedJSON.error ).toBeTrue(); 258 | expect( event ).toHaveStatus( 404 ); 259 | expect( returnedJSON.messages ).toHaveLengthGTE( 1 ); 260 | expect( returnedJSON.messages[ 1 ] ).toInclude( "user not found" ); 261 | } ); 262 | } ); 263 | 264 | when( "Including valid rantID for a non existing Rant", function(){ 265 | then( "I will get a 404 error", function(){ 266 | var event = put( 267 | "/api/v3/rants/#createUUID()#", 268 | { "userID" : createUUID(), "body" : "xsxswxws" } 269 | ); 270 | var returnedJSON = event.getRenderData().data; 271 | expect( returnedJSON.error ).toBeTrue(); 272 | expect( event ).toHaveStatus( 404 ); 273 | expect( returnedJSON.messages ).toHaveLengthGTE( 1 ); 274 | expect( returnedJSON.messages[ 1 ] ).toInclude( "rant not found" ); 275 | } ); 276 | } ); 277 | 278 | when( "I pass a valid body and userID and rantID", function(){ 279 | then( "I will update the Rant Successfully", function(){ 280 | var testRant = queryExecute( "select id,userId from rants limit 1" ); 281 | var event = put( 282 | "/api/v3/rants/#testRant.id#", 283 | { "body" : "xsxswxws", "userID" : testRant.userId } 284 | ); 285 | var returnedJSON = event.getRenderData().data; 286 | expect( returnedJSON.error ).toBeFalse(); 287 | expect( event.getStatusCode() ).toBe( 200 ); 288 | expect( returnedJSON.messages ).toHaveLengthGTE( 1 ); 289 | expect( returnedJSON.messages[ 1 ] ).toBe( "Rant Updated" ); 290 | } ); 291 | } ); 292 | } ); 293 | } ); 294 | 295 | story( "Delete a Rant", function(){ 296 | given( "I make a get call to /api/v3/rants/:rantID", function(){ 297 | when( "Using a get method", function(){ 298 | then( "I will hit the show action instead of the update action", function(){ 299 | var rantID = "1"; 300 | var event = get( "/api/v3/rants/#rantID#" ); 301 | expect( event.getCurrentAction() ).toBe( 302 | "show", 303 | "I expect to hit show action instead of the delete action due to the VERB, but I actually hit [#event.getCurrentAction()#]" 304 | ); 305 | } ); 306 | } ); 307 | 308 | when( "Using a post method", function(){ 309 | then( "I will hit the show action instead of the update action", function(){ 310 | var rantID = "1"; 311 | var event = post( "/api/v3/rants/#rantID#" ); 312 | expect( event.getCurrentAction() ).toBe( 313 | "onInvalidHTTPMethod", 314 | "I expect to hit onInvalidHTTPMethod action instead of the delete action due to the VERB, but I actually hit [#event.getCurrentAction()#]" 315 | ); 316 | } ); 317 | } ); 318 | 319 | when( "Including a space for rantID param", function(){ 320 | then( "I will hit the index action instead of the delete action", function(){ 321 | var rantID = " "; 322 | var event = get( "/api/v3/rants/#rantID#" ); 323 | expect( event.getCurrentAction() ).toBe( 324 | "index", 325 | "I expect to hit index action instead of the delete action due to the VERB, but I actually hit [#event.getCurrentAction()#]" 326 | ); 327 | } ); 328 | } ); 329 | 330 | when( "Including a non uuid rantID param", function(){ 331 | then( "I will get a 400 error", function(){ 332 | var rantID = "abc"; 333 | var event = delete( "/api/v3/rants/#rantID#" ); 334 | var returnedJSON = event.getRenderData().data; 335 | expect( returnedJSON.error ).toBeTrue(); 336 | expect( event ).toHaveStatus( 400 ); 337 | expect( returnedJSON.messages ).toHaveLengthGTE( 1 ); 338 | } ); 339 | } ); 340 | 341 | when( "Including valid rantID for a non existing Rant", function(){ 342 | then( "I will get a 404 error", function(){ 343 | var rantID = createUUID(); 344 | var event = delete( "/api/v3/rants/#rantID#" ); 345 | var returnedJSON = event.getRenderData().data; 346 | expect( returnedJSON.error ).toBeTrue(); 347 | expect( event ).toHaveStatus( 404 ); 348 | expect( returnedJSON.messages ).toHaveLengthGTE( 1 ); 349 | expect( returnedJSON.messages[ 1 ] ).toInclude( "rant not found" ); 350 | } ); 351 | } ); 352 | 353 | when( "I pass a valid rantID", function(){ 354 | then( "I will delete the rant successfully", function(){ 355 | var testUserId = queryExecute( "select id from users limit 1" ).id; 356 | var testRantId = getInstance( "RantService@v3" ).create( 357 | "my integration test", 358 | testUserId 359 | ).generatedKey; 360 | 361 | var event = delete( "/api/v3/rants/#testRantID#/delete" ); 362 | var returnedJSON = event.getRenderData().data; 363 | expect( returnedJSON.error ).toBeFalse(); 364 | expect( event.getStatusCode() ).toBe( 200 ); 365 | expect( returnedJSON.messages ).toHaveLengthGTE( 1 ); 366 | expect( returnedJSON.messages[ 1 ] ).toBe( "Rant Deleted" ); 367 | } ); 368 | } ); 369 | } ); 370 | } ); 371 | } ); 372 | } 373 | 374 | } 375 | -------------------------------------------------------------------------------- /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/fluentapi.sql: -------------------------------------------------------------------------------- 1 | # ************************************************************ 2 | # Sequel Ace SQL dump 3 | # Version 20033 4 | # 5 | # https://sequel-ace.com/ 6 | # https://github.com/Sequel-Ace/Sequel-Ace 7 | # 8 | # Host: 127.0.0.1 (MySQL 5.7.22) 9 | # Database: fluentapi 10 | # Generation Time: 2022-07-13 23:41:21 +0000 11 | # ************************************************************ 12 | 13 | USE fluentapi; 14 | 15 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; 16 | /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; 17 | /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; 18 | SET NAMES utf8mb4; 19 | /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; 20 | /*!40101 SET @OLD_SQL_MODE='NO_AUTO_VALUE_ON_ZERO', SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; 21 | /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; 22 | 23 | 24 | # Dump of table cfmigrations 25 | # ------------------------------------------------------------ 26 | 27 | DROP TABLE IF EXISTS `cfmigrations`; 28 | 29 | CREATE TABLE `cfmigrations` ( 30 | `name` varchar(190) NOT NULL, 31 | `migration_ran` datetime NOT NULL, 32 | PRIMARY KEY (`name`) 33 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 34 | 35 | LOCK TABLES `cfmigrations` WRITE; 36 | /*!40000 ALTER TABLE `cfmigrations` DISABLE KEYS */; 37 | 38 | INSERT INTO `cfmigrations` (`name`, `migration_ran`) 39 | VALUES 40 | ('2020_05_15_183916_users','2022-07-13 16:46:50'), 41 | ('2020_05_15_183939_rants','2022-07-13 16:46:50'), 42 | ('2020_05_15_184033_seedrants','2022-07-13 16:46:50'); 43 | 44 | /*!40000 ALTER TABLE `cfmigrations` ENABLE KEYS */; 45 | UNLOCK TABLES; 46 | 47 | 48 | # Dump of table rants 49 | # ------------------------------------------------------------ 50 | 51 | DROP TABLE IF EXISTS `rants`; 52 | 53 | CREATE TABLE `rants` ( 54 | `id` varchar(255) NOT NULL, 55 | `body` text NOT NULL, 56 | `createdDate` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, 57 | `updatedDate` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, 58 | `userId` varchar(255) NOT NULL, 59 | PRIMARY KEY (`id`), 60 | KEY `fk_rants_userId` (`userId`), 61 | CONSTRAINT `fk_rants_userId` FOREIGN KEY (`userId`) REFERENCES `users` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION 62 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 63 | 64 | LOCK TABLES `rants` WRITE; 65 | /*!40000 ALTER TABLE `rants` DISABLE KEYS */; 66 | 67 | INSERT INTO `rants` (`id`, `body`, `createdDate`, `updatedDate`, `userId`) 68 | VALUES 69 | ('0B068E2D-1E29-4049-BC5DDABE1491C25C','Captain America is the best superhero!','2022-07-13 16:46:49','2022-07-13 16:46:49','753C0511-257A-4674-92A7500293D60B28'), 70 | ('106EEAB6-44CA-43C2-AF85FA0CAAB86311','Testing test test','2022-07-13 16:46:49','2022-07-13 16:46:49','98BA3147-4071-4301-A011B1BD746077A3'), 71 | ('1E9B6514-13DC-44E5-8DA61A5CE3593041','This is the most amazing post in my life','2022-07-13 16:46:49','2022-07-13 16:46:49','E2F8B589-19E2-46DB-BE64D52483E6F6C6'), 72 | ('1FCCEC67-FB0E-4EE4-97C11403B1F890B3','I love kittens','2022-07-13 16:46:49','2022-07-13 16:46:49','147EB3B9-9102-4910-869CD8529F642AEE'), 73 | ('21626EB7-0AC7-4577-A6A10F929E916C79','What are you talking about!','2022-07-13 16:46:49','2022-07-13 16:46:49','147EB3B9-9102-4910-869CD8529F642AEE'), 74 | ('345AEC61-C741-4338-AF24BE1743C4F640','Scott seems to like my preso','2022-07-13 16:46:49','2022-07-13 16:46:49','98BA3147-4071-4301-A011B1BD746077A3'), 75 | ('381E5F38-7E87-4D3D-A7EA6BBC3C53D44C','I love soccer!','2022-07-13 16:46:49','2022-07-13 16:46:49','753C0511-257A-4674-92A7500293D60B28'), 76 | ('4D442EF9-B36B-4E3F-A2844FA2B77A3F76','This is the most amazing post in my life','2022-07-13 16:46:49','2022-07-13 16:46:49','9B232F88-8745-4872-A7D032BDA6AEE710'), 77 | ('576E42A0-98BA-4721-A4B3C6E8DC078455','Testing test test','2022-07-13 16:46:49','2022-07-13 16:46:49','9B232F88-8745-4872-A7D032BDA6AEE710'), 78 | ('6603FC86-279F-43AF-92C0DFABCD7BCD2B','Scott likes me preso','2022-07-13 16:46:49','2022-07-13 16:46:49','21B8DE1B-DE0D-456E-A56AF767AC72ADBF'), 79 | ('6E6D7AE4-E35B-4656-80D41B9F0A690561','Another rant','2022-07-13 16:46:49','2022-07-13 16:46:49','98BA3147-4071-4301-A011B1BD746077A3'), 80 | ('7A62D3C0-56EB-495F-8F401A5925E5E5A8','Why is this here!','2022-07-13 16:46:49','2022-07-13 16:46:49','753C0511-257A-4674-92A7500293D60B28'), 81 | ('81D28072-B557-4CD7-BDA6A728F40CD020','This post is not really good, it sucked!','2022-07-13 16:46:49','2022-07-13 16:46:49','21B8DE1B-DE0D-456E-A56AF767AC72ADBF'), 82 | ('9E88A5E4-D1E9-4314-A67D08C30E2CEFEC','Why are you doing this to me!','2022-07-13 16:46:49','2022-07-13 16:46:49','E2F8B589-19E2-46DB-BE64D52483E6F6C6'), 83 | ('A1AC85E5-8D2A-4751-B985100FA55A1495','I love espresso','2022-07-13 16:46:49','2022-07-13 16:46:49','98BA3147-4071-4301-A011B1BD746077A3'), 84 | ('B295B834-970D-4A88-B2C21DB1CE3C94EE','Please please please delete!','2022-07-13 16:46:49','2022-07-13 16:46:49','E2F8B589-19E2-46DB-BE64D52483E6F6C6'), 85 | ('D2EF83D6-1AE3-49D6-BCAA74422C8E2661','Scott seems to like my preso','2022-07-13 16:46:49','2022-07-13 16:46:49','147EB3B9-9102-4910-869CD8529F642AEE'), 86 | ('D967F1DA-9EB3-406F-A775D02203B46911','Captain America is the best superhero!','2022-07-13 16:46:49','2022-07-13 16:46:49','147EB3B9-9102-4910-869CD8529F642AEE'), 87 | ('E404B499-6C91-4512-9F8D34E6199016A8','I love espresso','2022-07-13 16:46:49','2022-07-13 16:46:49','753C0511-257A-4674-92A7500293D60B28'), 88 | ('F7A3E616-574E-4177-BB3E9E980218C125','Scott likes me preso','2022-07-13 16:46:49','2022-07-13 16:46:49','147EB3B9-9102-4910-869CD8529F642AEE'); 89 | 90 | /*!40000 ALTER TABLE `rants` ENABLE KEYS */; 91 | UNLOCK TABLES; 92 | 93 | 94 | # Dump of table users 95 | # ------------------------------------------------------------ 96 | 97 | DROP TABLE IF EXISTS `users`; 98 | 99 | CREATE TABLE `users` ( 100 | `id` varchar(255) NOT NULL, 101 | `username` varchar(255) NOT NULL, 102 | `email` varchar(255) NOT NULL, 103 | `password` varchar(255) NOT NULL, 104 | `createdDate` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, 105 | `updatedDate` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, 106 | PRIMARY KEY (`id`), 107 | UNIQUE KEY `username` (`username`), 108 | UNIQUE KEY `email` (`email`) 109 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 110 | 111 | LOCK TABLES `users` WRITE; 112 | /*!40000 ALTER TABLE `users` DISABLE KEYS */; 113 | 114 | INSERT INTO `users` (`id`, `username`, `email`, `password`, `createdDate`, `updatedDate`) 115 | VALUES 116 | ('147EB3B9-9102-4910-869CD8529F642AEE','luis','lmajano@ortussolutions.com','$2a$12$FE2J7ZLWaI2rSqejAu/84uLy7qlSufQsDsSE1lNNKyA05GG30gr8C','2022-07-13 16:46:49','2022-07-13 16:46:49'), 117 | ('21B8DE1B-DE0D-456E-A56AF767AC72ADBF','brad','brad@ortussolutions.com','$2a$12$Vbb4dYywI5X.1qKEV2mDzeOTZk3iHIDfEtz80SoMT0KkFWTkb.PB6','2022-07-13 16:46:49','2022-07-13 16:46:49'), 118 | ('753C0511-257A-4674-92A7500293D60B28','mike','mikep@netxn.com','$2a$12$WWUwFEAoDGx.vB0jE54xser1myMUSwUMYo/aNn0cSGa8l6DQe67Q2','2022-07-13 16:46:49','2022-07-13 16:46:49'), 119 | ('98BA3147-4071-4301-A011B1BD746077A3','gpickin','gavin@ortussolutions.com','$2a$12$JKiBJZF352Tfm/c3PpeslOBKRAwtXlwczMPKeUV1raD0d1cwh5B5.','2022-07-13 16:46:49','2022-07-13 16:46:49'), 120 | ('9B232F88-8745-4872-A7D032BDA6AEE710','scott','scott@scott.com','$2a$12$OjIpxecG9AlZTgVGV1jsvOegTwbqgJ29PlUkfomGsK/6hsVicsRW.','2022-07-13 16:46:49','2022-07-13 16:46:49'), 121 | ('E2F8B589-19E2-46DB-BE64D52483E6F6C6','javier','jquintero@ortussolutions.com','$2a$12$UIEOglSflvGUbn5sHeBZ1.sAlaoBI4rpNOCIk2vF8R2KKz.ihP9/W','2022-07-13 16:46:49','2022-07-13 16:46:49'); 122 | 123 | /*!40000 ALTER TABLE `users` ENABLE KEYS */; 124 | UNLOCK TABLES; 125 | 126 | 127 | 128 | /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; 129 | /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; 130 | /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; 131 | /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; 132 | /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; 133 | /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; 134 | -------------------------------------------------------------------------------- /workbench/setup-env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ########################################################################### 4 | # This file is called by Dockerfile to setup the image 5 | ########################################################################### 6 | echo "Starting up container in test mode" 7 | # We send up our "testing" flag to prevent the default CommandBox image run script from begining to tail output, thus stalling our build 8 | export IMAGE_TESTING_IN_PROGRESS=true 9 | # Run our normal build script, which will warm up our server and add it to the image 10 | ${BUILD_DIR}/run.sh 11 | sleep 15 12 | 13 | # Stop our server 14 | cd ${APP_DIR} && box server stop 15 | # Remove our testing flag, so that our container will start normally when its run 16 | unset IMAGE_TESTING_IN_PROGRESS 17 | echo "Container successfully warmed up" 18 | 19 | echo "Environment setup complete" 20 | --------------------------------------------------------------------------------