├── .env.example ├── .github └── workflows │ └── release-npm.yaml ├── .gitignore ├── .npmignore ├── .npmrc ├── LICENSE ├── README.md ├── bin └── index.js ├── commands ├── create │ ├── cloud_function │ │ └── index.js │ ├── cron │ │ └── index.js │ ├── route │ │ └── index.js │ ├── shared_module │ │ └── index.js │ ├── socket │ │ └── index.js │ ├── subscribers │ │ └── index.js │ └── topic │ │ └── index.js ├── create_custom_domain │ └── index.js ├── deploy │ └── index.js ├── deploy_static │ └── index.js ├── helpers │ ├── create_api_url.js │ ├── create_server_url.js │ ├── credentials.js │ ├── handle_error.js │ ├── index.js │ └── validations │ │ ├── validate_project_folder.js │ │ ├── validate_project_name.js │ │ ├── validate_project_structure │ │ ├── errors │ │ │ └── index.js │ │ ├── index.js │ │ ├── validate_apis │ │ │ ├── index.js │ │ │ └── validate_api_routes │ │ │ │ └── index.js │ │ ├── validate_cloud_functions │ │ │ └── index.js │ │ ├── validate_crons │ │ │ └── index.js │ │ ├── validate_external_topics │ │ │ └── index.js │ │ ├── validate_shared │ │ │ └── index.js │ │ ├── validate_sockets │ │ │ └── index.js │ │ └── validate_topics │ │ │ └── index.js │ │ ├── validate_resources_instances_names.js │ │ ├── validate_static_domain_name.js │ │ └── validate_static_folder_name.js ├── list_organizations.js ├── local │ ├── build │ │ └── index.js │ ├── constants │ │ └── index.js │ ├── delete │ │ └── index.js │ ├── helpers │ │ ├── add_to_package_json │ │ │ └── index.js │ │ ├── build_apis │ │ │ ├── build.js │ │ │ └── index.js │ │ ├── build_cloud_functions │ │ │ └── index.js │ │ ├── build_crons │ │ │ └── index.js │ │ ├── build_less_dependencies │ │ │ └── index.js │ │ ├── build_path │ │ │ └── index.js │ │ ├── build_shared_code │ │ │ └── index.js │ │ ├── build_sockets │ │ │ └── index.js │ │ ├── build_sqlite_database_dependency │ │ │ ├── index.js │ │ │ └── query.sql │ │ ├── build_topics │ │ │ └── index.js │ │ ├── check_resources │ │ │ └── index.js │ │ ├── convert_snake_to_camel_case │ │ │ └── index.js │ │ ├── create_dotenv_based_on_less_config │ │ │ └── index.js │ │ ├── get_yarn_path │ │ │ └── index.js │ │ ├── less_app_config_file │ │ │ └── index.js │ │ ├── map_dirs_recursive │ │ │ └── index.js │ │ └── run_app │ │ │ └── index.js │ ├── list_projects │ │ └── index.js │ └── run_app │ │ └── index.js ├── login_profile │ └── get_login_profile.js ├── projects │ ├── delete.js │ ├── get_all.js │ ├── get_by_id.js │ └── logs │ │ └── fetch_and_log_function_logs.js ├── service │ └── api.js ├── user │ ├── create_account.js │ ├── create_session.js │ └── forgot_password.js └── view_user_profile.js ├── package.json ├── scripts └── release │ └── release.sh ├── templates └── ts │ ├── api │ └── get │ │ ├── basic.ts │ │ └── with_logger_middleware.ts │ ├── cloud_function │ └── basic.ts │ ├── cron │ └── basic.ts │ ├── socket │ ├── channel │ │ └── basic.ts │ ├── connect │ │ └── basic.ts │ └── disconnect │ │ └── basic.ts │ └── topic │ └── subscriber │ └── basic.ts ├── utils ├── check_for_updates.js ├── config.js ├── directory_has_less_folder.js └── templates.js └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | LESS_SERVER_BASE_URL="http://localhost:3000" 2 | LESS_SERVER_SOCKET_URL="ws://localhost:3000" 3 | LESS_API_BASE_URL="http://localhost:3333" 4 | -------------------------------------------------------------------------------- /.github/workflows/release-npm.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy to NPM 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | publish: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 # Fetch all history for all tags and branches 19 | 20 | - name: Setup Node.js 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: "20.x" 24 | registry-url: "https://registry.npmjs.org" 25 | 26 | - name: Set git user 27 | run: | 28 | git config --local user.email "less-ci@github.com" 29 | git config --local user.name "GitHub Action" 30 | 31 | - name: Execute Publish Script 32 | run: ./scripts/release/release.sh 33 | env: 34 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 35 | GH_TOKEN: ${{ github.token }} 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node artifact files 2 | node_modules/ 3 | 4 | .env 5 | .vscode/ 6 | 7 | # Generated by MacOS 8 | .DS_Store 9 | 10 | # Log files 11 | *.log -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # Set the registry 2 | @chuva.io:registry=https://registry.npmjs.org/ 3 | 4 | # Enable strict SSL 5 | strict-ssl=true 6 | 7 | # Save exact versions 8 | save-exact=true 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm version](https://badge.fury.io/js/@chuva.io%2Fless-cli.svg)](https://badge.fury.io/js/@chuva.io%2Fless-cli) 2 | 3 | `less-cli` is a CLI tool that allow you to deploy your Less projects to AWS while providing several other tools to facilitate your interaction with Less. 4 | 5 | - [Learn about Less](https://chuva-io.notion.site/Less-44d98337e08a46af934364700da05e3a) 6 | - [Less developer documentation](https://docs.less.chuva.io/) 7 | 8 | # Commands 9 | 10 | ## `register` 11 | 12 | To start using LESS, first you must create your account, you can do this using the command `register`. 13 | 14 | ```bash 15 | $ less-cli register 16 | ``` 17 | 18 | #### Example: 19 | ```bash 20 | $ less-cli register 21 | 22 | [less-cli] Enter your name: Cesaria Evora 23 | [less-cli] Enter your email: cesaria@chuva.io 24 | [less-cli] Enter your password: ************ 25 | [less-cli] Enter the verification code sent to your email: ****** 26 | [less-cli] Account verified! Please check your email for your Less credentials. 27 | ``` 28 | 29 | ## `login` 30 | 31 | After your account being created you need to sign in in order to retrieve your **LESS token** that will enable you to use all the rest of the commands. To do this you need to use the command `login`. 32 | 33 | *Note: The LESS token will be exported to your environment variables* 34 | 35 | ```bash 36 | $ less-cli login 37 | ``` 38 | 39 | #### Example: 40 | ```bash 41 | $ less-cli login 42 | 43 | [less-cli] Enter your email: cesaria@chuva.io 44 | [less-cli] Enter your password: ************ 45 | [less-cli] Login successful! Your LESS_TOKEN has been exported to your environment. 46 | ``` 47 | 48 | ## `forgot-password` 49 | 50 | If you've forgotten your password, this command enables you to recover it by sending a verification code to your email, which is then required to change your password. 51 | 52 | To initiate the password recovery process, use the command `forgot-password`. 53 | 54 | ```bash 55 | $ less-cli forgot-password 56 | ``` 57 | 58 | #### Example: 59 | ```bash 60 | $ less-cli forgot-password 61 | 62 | [less-cli] Enter your email: cesaria@chuva.io 63 | [less-cli] Enter the verification code sent to your email: ****** 64 | [less-cli] Enter new password: ************ 65 | [less-cli] Password reset successfully. 66 | ``` 67 | 68 | ## `deploy` 69 | 70 | The `deploy` command allow you to deploy your Less project to AWS. 71 | 72 | ```bash 73 | $ less-cli deploy 74 | ``` 75 | 76 | #### Example: 77 | 78 | ```bash 79 | $ less-cli deploy my-application 80 | 81 | [less] Building... ⚙️ 82 | [less] Build complete ✅ 83 | [less] Deploying... 🚀 84 | [less] Deployment complete ✅ 85 | [less] Resources 86 | [less] - API URLs 87 | [less] - chat: https://a2m1n3.execute-api.eu-west-1.amazonaws.com 88 | [less] - webhooks: https://n2s9n5.execute-api.eu-west-1.amazonaws.com 89 | [less] - Web Socket URLs 90 | [less] - realtime_chat: wss://10l06n.execute-api.eu-west-1.amazonaws.com 91 | [less] 🇨🇻 92 | ``` 93 | 94 | ### Parameters 95 | 96 | `` 97 | The name of your Less project. 98 | 99 | *Note: Supports alphanumeric characters and "-".* 100 | 101 | ### Options 102 | 103 | ` --static ` 104 | 105 | Less also allow you to deploy your static websites, with the option `--static` that proceedes the `deploy` command. 106 | 107 | ```bash 108 | $ less-cli deploy --static 109 | ``` 110 | 111 | #### Example: 112 | ```bash 113 | $ less-cli deploy --static my-application 114 | 115 | [less] Building... ⚙️ 116 | [less] Build complete ✅ 117 | [less] Deploying... 🚀 118 | [less] Deployment complete ✅ 119 | [less] Resources 120 | [less] - Websites URLs 121 | [less] - http://my-application-demo-website.s3-website-eu-west-1.amazonaws.com 122 | [less] 🇨🇻 123 | ``` 124 | 125 | ## `list` 126 | 127 | The `list` command allow you to fetch and list all your projects deployed to AWS. 128 | 129 | ```bash 130 | $ less-cli list 131 | ``` 132 | 133 | #### Example: 134 | ```bash 135 | $ less-cli list 136 | 137 | ID: demo-api 138 | Created At: 2023-11-14T12:20:51.828Z 139 | Updated At: 2023-11-14T13:38:00.619Z 140 | 141 | ID: ping 142 | Created At: 2023-11-09T20:20:42.183Z 143 | Updated At: 2023-11-10T11:41:38.740Z 144 | 145 | ID: samba 146 | Created At: 2023-11-04T11:44:39.595Z 147 | Updated At: 2023-11-08T11:44:45.850Z 148 | ``` 149 | 150 | ## `delete` 151 | 152 | The `delete` command allow you to delete a project deployed to AWS. 153 | 154 | ```bash 155 | $ less-cli delete 156 | ``` 157 | 158 | #### Example: 159 | ```bash 160 | $ less-cli delete my-api 161 | 162 | [less-cli] The process has started. Wait a few minutes and list the projects to see the changes. 163 | ``` 164 | 165 | ### Parameters 166 | 167 | `` 168 | The name of your Less project. 169 | 170 | *Note: Supports alphanumeric characters and "-".* 171 | 172 | ## `list resources` 173 | 174 | The command `list resources` allow you to list your project resources after you deployed it. On this list includes your apis and websockets endpoints. 175 | 176 | ```bash 177 | $ less-cli list resources 178 | ``` 179 | 180 | #### Example: 181 | ```bash 182 | $ less-cli list resources my-api 183 | 184 | [less-cli] API URLs 185 | [less-cli] - Demo: https://3izstmbced.execute-api.eu-west-1.amazonaws.com/production 186 | [less-cli] WEBSOCKET URLs 187 | [less-cli] - ChatSocketApi: wss://pr9fbdgwve.execute-api.eu-west-1.amazonaws.com/production 188 | ``` 189 | 190 | ### Parameters 191 | 192 | `` 193 | The name of your Less project. 194 | 195 | *Note: Supports alphanumeric characters and "-".* 196 | 197 | ## `log` 198 | 199 | The command `log` is used to fetches and logs the function logs based on the specified project and function path. 200 | 201 | ```bash 202 | $ less-cli log --project --path 203 | ``` 204 | 205 | #### Example: 206 | ```bash 207 | $ less-cli log --project my-api --path apis/demo/hello/get 208 | 209 | 2023-11-29 15:00:22.938 START RequestId: 15e6099b-b101-4574-ab62-b848c967ee29 Version: $LATEST 210 | 2023-11-29 15:00:22.956 2023-11-29T16:00:22.956Z 15e6099b-b101-4574-ab62-b848c967ee29 ERROR Error: test error 211 | at Object.process (/var/task/get.js:9:15) 212 | at Runtime.exports.handler (/var/task/handler_get.js:27:38) 213 | at Runtime.handleOnceNonStreaming (file:///var/runtime/index.mjs:1173:29) 214 | 2023-11-29 15:00:22.997 END RequestId: 15e6099b-b101-4574-ab62-b848c967ee29 215 | 2023-11-29 15:00:22.997 REPORT RequestId: 15e6099b-b101-4574-ab62-b848c967ee29 Duration: 58.87 ms Billed Duration: 59 ms Memory Size: 128 MB Max Memory Used: 58 MB Init Duration: 177.97 ms 216 | 2023-11-29 15:00:28.006 2023-11-29T16:00:28.006Z 009b82d3-41a6-4b3e-abba-35e6d1628939 ERROR Error: test error 217 | at Object.process (/var/task/get.js:9:15) 218 | at Runtime.exports.handler (/var/task/handler_get.js:27:38) 219 | at Runtime.handleOnceNonStreaming (file:///var/runtime/index.mjs:1173:29) 220 | 2023-11-29 15:00:28.006 START RequestId: 009b82d3-41a6-4b3e-abba-35e6d1628939 Version: $LATEST 221 | 2023-11-29 15:00:28.017 END RequestId: 009b82d3-41a6-4b3e-abba-35e6d1628939 222 | 2023-11-29 15:00:28.017 REPORT RequestId: 009b82d3-41a6-4b3e-abba-35e6d1628939 Duration: 12.37 ms Billed Duration: 13 ms Memory Size: 128 MB Max Memory Used: 59 MB 223 | ``` 224 | 225 | ### Options 226 | 227 | The command `log` requires two options. 228 | 229 | `--project ` 230 | This option allow you to specify the name of your project for which you want to list the logs. 231 | 232 | `--path ` 233 | This option allow you to specify the path of the function for which you want to see its logs. 234 | 235 | --- 236 | 237 | Do more with Less. 238 | 🇨🇻 239 | -------------------------------------------------------------------------------- /bin/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { fileURLToPath } from 'url'; 3 | import path, { dirname } from 'path'; 4 | import fs from 'fs'; 5 | import { Command, Option } from 'commander'; 6 | 7 | import deploy from '../commands/deploy/index.js'; 8 | import deploy_static from '../commands/deploy_static/index.js'; 9 | import create_route from '../commands/create/route/index.js'; 10 | import create_socket from '../commands/create/socket/index.js'; 11 | import create_topic from '../commands/create/topic/index.js'; 12 | import create_subscribers from '../commands/create/subscribers/index.js'; 13 | import create_cron from '../commands/create/cron/index.js'; 14 | import create_shared_module from '../commands/create/shared_module/index.js'; 15 | import create_cloud_function from '../commands/create/cloud_function/index.js'; 16 | import get_all from '../commands/projects/get_all.js'; 17 | import get_by_id from '../commands/projects/get_by_id.js'; 18 | import create_account from '../commands/user/create_account.js'; 19 | import create_session from '../commands/user/create_session.js'; 20 | import forgot_password from '../commands/user/forgot_password.js'; 21 | import create_custom_domain from '../commands/create_custom_domain/index.js'; 22 | import fetch_and_log_function_logs from '../commands/projects/logs/fetch_and_log_function_logs.js'; 23 | import delete_project from '../commands/projects/delete.js'; 24 | import check_for_updates from '../utils/check_for_updates.js'; 25 | import directory_has_less_folder from '../utils/directory_has_less_folder.js'; 26 | import chalk from 'chalk'; 27 | import validate_project_structure from '../commands/helpers/validations/validate_project_structure/index.js'; 28 | import get_login_profile from '../commands/login_profile/get_login_profile.js'; 29 | import run_app from '../commands/local/run_app/index.js'; 30 | import build from '../commands/local/build/index.js'; 31 | import delete_local_project from '../commands/local/delete/index.js'; 32 | import list_local_projects from '../commands/local/list_projects/index.js'; 33 | import list_organizations from '../commands/list_organizations.js'; 34 | import view_user_profile from '../commands/view_user_profile.js'; 35 | 36 | const program = new Command(); 37 | 38 | // Get the directory of the current module file 39 | const __dirname = dirname(fileURLToPath(import.meta.url)); 40 | 41 | // Get package.json version 42 | const packagePath = path.join(__dirname, '..', 'package.json'); 43 | const packageContent = JSON.parse(fs.readFileSync(packagePath).toString()); 44 | const version = packageContent?.version; 45 | 46 | const LANGUAGE_OPTIONS = ['js', 'ts', 'py']; 47 | 48 | // Setting the name and description of the CLI tool 49 | program 50 | .name('less-cli') 51 | .description('CLI to interact with Less') 52 | .version(version) 53 | .option('-o --organization ', 'Organization ID') 54 | .usage('[COMMAND]') 55 | .hook('preAction', (command) => { 56 | if( 57 | !directory_has_less_folder() 58 | && ['deploy', 'build'].includes(command.args[0]) 59 | ) { 60 | console.log( 61 | chalk.red(`ERROR: There is no 'less' folder in the current directory. 62 | In order to deploy your project navigate to the correct directory and try again.`), 63 | ); 64 | process.exit(1); 65 | } 66 | }) 67 | .hook('postAction', async (command) => { 68 | if(!directory_has_less_folder() && command.args[0] !== 'deploy') { 69 | console.log( 70 | chalk.yellowBright('WARNING: You are using Less commands in a directory with no `less` folder.\n'), 71 | ); 72 | } 73 | await check_for_updates(); 74 | process.exit(process.exitCode); 75 | }); 76 | 77 | program 78 | .command('me') 79 | .description('View the user profile for the logged in user.') 80 | .action(view_user_profile); 81 | 82 | program 83 | .command('list-organizations') 84 | .description('List all organizations that you are a part of.') 85 | .action(list_organizations); 86 | 87 | program 88 | .command('deploy ') 89 | .description('Deploy your less project.') 90 | .option('--static', 'Deploy your less static websites') 91 | .action(async (project_name, options, command) => { 92 | const organization_id = command.parent.opts().organization; 93 | if (options.static) { 94 | await deploy_static(organization_id, project_name); 95 | return; 96 | }; 97 | 98 | try { 99 | validate_project_structure(process.cwd()); 100 | } catch (error) { 101 | console.log(chalk.yellowBright('[less-cli]'), chalk.redBright('ERROR:', error.message)); 102 | process.exit(1); 103 | } 104 | 105 | await deploy(organization_id, project_name); 106 | }) 107 | .hook('postAction',(command) => { 108 | if (!process.exitCode && !command.opts().static) { 109 | const project_name = command.args[0]; 110 | console.log(`\nYou can visit https://dashboard.less.chuva.io/projects/${project_name} to view your project's resources, logs, and metrics from your browser.`); 111 | } 112 | }); 113 | 114 | program 115 | .command('domains') 116 | .description('Use custom domains') 117 | .option('--project-name ', 'Name of your project') 118 | .option('--static-name ', 'Name of your static folder') 119 | .option('--custom-domain ', 'Your custom domain') 120 | .action(create_custom_domain); 121 | 122 | program 123 | .command('list') 124 | .description('List all projects.') 125 | .option('--local', 'Flag to only list the projects that are hosted locally.') 126 | .action(async (options, command) => { 127 | const organization_id = command.parent.opts().organization; 128 | if (options.local) { 129 | list_local_projects(); 130 | return; 131 | } 132 | await get_all(organization_id); 133 | }) 134 | .command('resources ') 135 | .description('List resources by project_id') 136 | .action(get_by_id); 137 | 138 | program 139 | .command('log') 140 | .description('List logs by project. Example usage: less-cli log --project hello-api --function apis/demo/hello/get') 141 | .option('--project ', 'Specify the name of your project for which you want to list the logs') 142 | .option('--function ', 'Specify the path of the function for which you want to log') 143 | .action(fetch_and_log_function_logs); 144 | 145 | program 146 | .command('register') 147 | .description('Create your less account') 148 | .action(create_account); 149 | 150 | program 151 | .command('forgot-password') 152 | .description("This command will send a message to the end user with a confirmation code that is required to change the user's password.") 153 | .action(forgot_password); 154 | 155 | program 156 | .command('login') 157 | .description('Log in with email and password.') 158 | .option('-u, --user ', 'User email address') 159 | .option('-p, --password ', 'User password', '*') 160 | .action(create_session); 161 | 162 | program 163 | .command('aws-access') 164 | .description('Access your Less-managed AWS account directly via the AWS console.') 165 | .action(get_login_profile); 166 | 167 | program 168 | .command('delete ') 169 | .option('--local', 'Flag to specify that the project to delete is hosted locally.') 170 | .description('Delete your project. Example usage: less-cli delete hello-api') 171 | .action(async (project_name, options, command) => { 172 | const organization_id = command.parent.opts().organization; 173 | if (options.local) { 174 | await delete_local_project(project_name); 175 | return; 176 | } 177 | await delete_project(organization_id, project_name); 178 | }); 179 | 180 | 181 | program 182 | .command('build ') 183 | .description('Build your Less project locally for offline development.') 184 | .action(async (project_name) => { 185 | try { 186 | validate_project_structure(process.cwd()); 187 | } catch (error) { 188 | console.log(chalk.yellowBright('[less-cli]'), chalk.redBright('ERROR:', error.message)); 189 | process.exit(1); 190 | } 191 | 192 | await build(project_name); 193 | }); 194 | 195 | program 196 | .command('run ') 197 | .description('Run your Less project locally. You can create a build using the `build ` command.') 198 | .action(run_app); 199 | 200 | const create = program 201 | .command('create') 202 | .summary('Creates your Less files/resources and boilerplate code for you.') 203 | .description('Streamline your development by creating your Less files/resources and boilerplate code automatically.'); 204 | 205 | create 206 | .command('route') 207 | .summary('Creates your HTTP routes.') 208 | .description('Creates your HTTP routes.\n\nRequired options: For options marked as required, if you do not specify an option you will be asked to specify it in interactive mode instead.\n\nRead the REST API Documentation: https://less.chuva.io/rest-apis') 209 | .option('-n, --name ', 'Required: The name of the API to create the route for. (E.g. "store_api")') 210 | .option('-p, --path ', 'Required: The HTTP route path to create. (E.g. "/orders/{order_id}")') 211 | .addOption( 212 | new Option('-l, --language ', 'Required: The programming language to use for the code.') 213 | .choices(LANGUAGE_OPTIONS) 214 | ) 215 | .addOption( 216 | new Option('-v, --verb ', 'Required: The HTTP verb to use for the route.') 217 | .choices(['get', 'post', 'put', 'patch', 'delete']) 218 | ) 219 | .action(create_route); 220 | 221 | create 222 | .command('socket') 223 | .summary('Creates your Web Sockets and socket channels or adds channels to existing sockets.') 224 | .description('Creates your Web Sockets and socket channels or adds channels to existing sockets.\n\nRequired options: For options marked as required, if you do not specify an option you will be asked to specify it in interactive mode instead.\n\nRead the Web Socket Documentation: https://less.chuva.io/web-sockets') 225 | .option('-n, --name ', 'Required: The name of the Web Socket to create or to add channels to. (E.g. "--name realtime_chat")') 226 | .addOption( 227 | new Option('-l, --language ', 'Required: The programming language to use for the code.') 228 | .choices(LANGUAGE_OPTIONS) 229 | ) 230 | .option('-c, --channels ', 'A list of channels to create, allowing clients to send messages to the server. (E.g. "--channels public_chatroom private_chatroom")') 231 | .action(create_socket); 232 | 233 | create 234 | .command('topic') 235 | .summary('Creates Topics and Subscribers.') 236 | .description('Creates Topics and Subscribers.\n\nRequired options: For options marked as required, if you do not specify an option you will be asked to specify it in interactive mode instead.\n\nRead the Topics / Subscribers (Pub / Sub) Documentation: https://less.chuva.io/topics_subscribers') 237 | .option('-n, --name ', 'Required: The name of the Topic to create or to add Subscribers to. (E.g. "--name user_created")') 238 | .addOption( 239 | new Option('-l, --language ', 'Required: The programming language to use for the code.') 240 | .choices(LANGUAGE_OPTIONS) 241 | ) 242 | .option('-s, --subscribers ', 'A list of Subscribers to create for a Topic. (E.g. "--subscribers send_welcome_email send_to_analytics")') 243 | .option('-ex, --external-topic ', 'The name of the external service to connect to. Use this option to create subscribers to external services. (E.g. "--external-name iot_sensor_stream_service")') 244 | .action(create_topic); 245 | 246 | create 247 | .command('subscribers') 248 | .summary('Creates Subscribers to Topics.') 249 | .description('Creates Subscribers to Topics.\n\nRead the Topics / Subscribers (Pub / Sub) Documentation: https://less.chuva.io/topics_subscribers') 250 | .option('-n, --names ', 'Required: A list of Subscribers to create. (E.g. "--names send_welcome_email send_to_analytics")') 251 | .option('-t, --topic ', 'Required: The name of the Topic to create or subscribe to. (E.g. "--name user_created")') 252 | .addOption( 253 | new Option('-l, --language ', 'Required: The programming language to use for each subscriber\'s code.') 254 | .choices(LANGUAGE_OPTIONS) 255 | ) 256 | .option('-ex, --external-topic ', 'The name of the external service to subscribe to. Use this option to create subscribers to external services. (E.g. "--external-name iot_sensor_stream_service")') 257 | .action(create_subscribers); 258 | 259 | create 260 | .command('cron') 261 | .summary('Creates your CRON Jobs.') 262 | .description('Creates your CRON Jobs.\n\nRead the CRON Jobs Documentation: https://less.chuva.io/cron-jobs') 263 | .option('-n, --name ', 'Required: Enter The name of the CRON Job to create. (E.g. "--name generate_report")') 264 | .addOption( 265 | new Option('-l, --language ', 'Required: The programming language to use for the code.') 266 | .choices(LANGUAGE_OPTIONS) 267 | ) 268 | .action(create_cron); 269 | 270 | create 271 | .command('shared-module') 272 | .summary('Creates your Shared Code Modules.') 273 | .description('Creates your Shared Code Modules.\n\nRead the Shared Modules Documentation: https://less.chuva.io/shared-modules') 274 | .option('-n, --name ', 'Required: The name of the Module to create. (E.g. "--name orm_models")') 275 | .addOption( 276 | new Option('-l, --language ', 'Required: The programming language to use for the code.') 277 | .choices(LANGUAGE_OPTIONS) 278 | ) 279 | .action(create_shared_module); 280 | 281 | create 282 | .command('cloud-function') 283 | .summary('Creates your Cloud Functions.') 284 | .description('Creates your Cloud Functions.\n\nRead the Cloud Functions Documentation: https://less.chuva.io/cloud-functions') 285 | .option('-n, --name ', 'Required: The name of the Cloud Function to create. (E.g. "--name add_numbers")') 286 | .addOption( 287 | new Option('-l, --language ', 'Required: The programming language to use for the code.') 288 | .choices(LANGUAGE_OPTIONS) 289 | ) 290 | .action(create_cloud_function); 291 | 292 | // Parsing the command-line arguments and executing the corresponding actions 293 | program.parse(); 294 | -------------------------------------------------------------------------------- /commands/create/cloud_function/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | inquire_unanswered_questions, 3 | create_file, 4 | language_file_names 5 | } from '../../helpers/index.js'; 6 | import { cloud_function as cloud_function_templates } from '../../../utils/templates.js'; 7 | 8 | const questions = [ 9 | { 10 | type: 'input', 11 | name: 'name', 12 | message: 'Enter the name of the Cloud Function to create. (E.g. "add_numbers")', 13 | default: 'add_numbers', 14 | validate: function (input) { 15 | if (!/^[a-zA-Z][-a-zA-Z0-9_]*$/.test(input)) { 16 | return 'The Cloud Function should only contain alphanumeric characters, "-", or "_"'; 17 | } 18 | return true; 19 | }, 20 | }, 21 | { 22 | type: 'list', 23 | name: 'language', 24 | message: "Enter the programming language to use for the code.", 25 | choices: ['js', 'ts', 'py'] 26 | } 27 | ] 28 | 29 | export default async (options) => { 30 | const answers = await inquire_unanswered_questions(options, questions); 31 | const file_name = language_file_names[answers.language]; 32 | const folder_path = `less/functions/${answers.name}`; 33 | 34 | const template_map = { 35 | js: js_template, 36 | ts: cloud_function_templates.load_function_ts(), 37 | py: py_template 38 | }; 39 | 40 | const file_content = template_map[answers.language]; 41 | create_file(folder_path, file_name, file_content); 42 | }; 43 | 44 | const js_template = `exports.process = ({ a, b }) => { 45 | return a + b; 46 | }; 47 | `; 48 | 49 | const py_template = `def process(data): 50 | return data['a'] + data['b'] 51 | `; 52 | -------------------------------------------------------------------------------- /commands/create/cron/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | inquire_unanswered_questions, 3 | create_file, 4 | language_file_names 5 | } from '../../helpers/index.js'; 6 | 7 | import { cron as cron_templates } from '../../../utils/templates.js'; 8 | 9 | const questions = [ 10 | { 11 | type: 'input', 12 | name: 'name', 13 | message: 'Enter the name of the CRON Job to create. (E.g. "generate_report")', 14 | default: 'generate_report', 15 | validate: function (input) { 16 | if (!/^[a-zA-Z][-a-zA-Z0-9_]*$/.test(input)) { 17 | return 'The CRON Job should only contain alphanumeric characters, "-", or "_"'; 18 | } 19 | return true; 20 | }, 21 | }, 22 | { 23 | type: 'list', 24 | name: 'language', 25 | message: "Enter the programming language to use for the code.", 26 | choices: ['js', 'ts', 'py'] 27 | } 28 | ] 29 | 30 | export default async (options) => { 31 | const answers = await inquire_unanswered_questions(options, questions); 32 | const file_name = language_file_names[answers.language]; 33 | const folder_path = `less/crons/${answers.name}`; 34 | 35 | const template_map = { 36 | js: js_template, 37 | ts: cron_templates.load_cron_ts(), 38 | py: py_template 39 | }; 40 | 41 | const file_content = template_map[answers.language]; 42 | create_file(folder_path, file_name, file_content); 43 | }; 44 | 45 | const js_template = `exports.process = async () => { 46 | console.log('Running CRON job...'); 47 | }; 48 | `; 49 | 50 | const py_template = `def process(): 51 | print('Running CRON job...') 52 | `; 53 | -------------------------------------------------------------------------------- /commands/create/route/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | inquire_unanswered_questions, 3 | create_file 4 | } from '../../helpers/index.js'; 5 | 6 | import { api as api_templates } from '../../../utils/templates.js'; 7 | 8 | const questions = [ 9 | { 10 | type: 'input', 11 | name: 'name', 12 | message: 'Enter the name of the API to create the route for. (E.g. "store_api")', 13 | validate: function (input) { 14 | if (!/^[a-zA-Z][-a-zA-Z0-9_]*$/.test(input)) { 15 | return 'The API name should only contain alphanumeric characters, "-", or "_"'; 16 | } 17 | return true; 18 | }, 19 | }, 20 | { 21 | type: 'input', 22 | name: 'path', 23 | message: "Enter the HTTP route path to create. (E.g. '/orders/{order_id}')", 24 | }, 25 | { 26 | type: 'list', 27 | name: 'language', 28 | message: "Enter the programming language to use for the code.", 29 | choices: ['js', 'ts', 'py'] 30 | }, 31 | { 32 | type: 'list', 33 | name: 'verb', 34 | message: "The HTTP verb to use for the route.", 35 | choices: ['get', 'post', 'put', 'patch', 'delete'] 36 | } 37 | ] 38 | 39 | export default async (options) => { 40 | const answers = await inquire_unanswered_questions(options, questions); 41 | const folder_path = `less/apis/${answers.name}${answers.path}`; 42 | const file_name = `${answers.verb}.${answers.language}`; 43 | 44 | const template_map = { 45 | js: js_template, 46 | ts: api_templates.load_route_ts(), 47 | py: py_template 48 | }; 49 | 50 | const file_content = template_map[answers.language]; 51 | create_file(folder_path, file_name, file_content); 52 | }; 53 | 54 | const js_template = `exports.process = async (request, response) => { 55 | response.body = 'Hello, world.'; 56 | return response; 57 | }; 58 | `; 59 | 60 | const py_template = `def process(request, response): 61 | response['body'] = 'Hello, world.' 62 | return response 63 | `; 64 | -------------------------------------------------------------------------------- /commands/create/shared_module/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | inquire_unanswered_questions, 3 | create_file, 4 | language_file_names 5 | } from '../../helpers/index.js'; 6 | 7 | const questions = [ 8 | { 9 | type: 'input', 10 | name: 'name', 11 | message: 'Enter the name of the Module to create. (E.g. "orm_models")', 12 | default: 'orm_models', 13 | validate: function (input) { 14 | if (!/^[a-zA-Z][-a-zA-Z0-9_]*$/.test(input)) { 15 | return 'The Shared Module should only contain alphanumeric characters, "-", or "_"'; 16 | } 17 | return true; 18 | }, 19 | }, 20 | { 21 | type: 'list', 22 | name: 'language', 23 | message: "Enter the programming language to use for the code.", 24 | choices: ['js', 'ts', 'py'] 25 | } 26 | ] 27 | 28 | export default async (options) => { 29 | const answers = await inquire_unanswered_questions(options, questions); 30 | const file_name = language_file_names[answers.language]; 31 | const folder_path = `less/shared/${answers.name}`; 32 | create_file(folder_path, file_name); 33 | }; 34 | -------------------------------------------------------------------------------- /commands/create/socket/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | inquire_unanswered_questions, 3 | create_file , 4 | language_file_names 5 | } from '../../helpers/index.js'; 6 | 7 | import { socket as socket_templates } from '../../../utils/templates.js'; 8 | 9 | const questions = [ 10 | { 11 | type: 'input', 12 | name: 'name', 13 | message: 'Enter the name of the Web Socket to create or to add channels to. (E.g. "realtime_chat")', 14 | default: 'realtime_chat', 15 | validate: function (input) { 16 | if (!/^[a-zA-Z][-a-zA-Z0-9_]*$/.test(input)) { 17 | return 'The Web Socket name should only contain alphanumeric characters, "-", or "_"'; 18 | } 19 | return true; 20 | }, 21 | }, 22 | { 23 | type: 'list', 24 | name: 'language', 25 | message: "Enter the programming language to use for the code.", 26 | choices: ['js', 'ts', 'py'] 27 | } 28 | ] 29 | 30 | export default async (options) => { 31 | const answers = await inquire_unanswered_questions(options, questions); 32 | const file_name = language_file_names[answers.language]; 33 | 34 | const connect_folder_path = `less/sockets/${answers.name}/connect`; 35 | const connect_file_content = connect_templates[answers.language]; 36 | create_file(connect_folder_path, file_name, connect_file_content); 37 | 38 | const disconnect_folder_path = `less/sockets/${answers.name}/disconnect`; 39 | const disconnect_file_content = disconnect_templates[answers.language]; 40 | create_file(disconnect_folder_path, file_name, disconnect_file_content); 41 | 42 | if (answers.channels) { 43 | answers.channels.forEach(channel => { 44 | let channel_folder_path = `less/sockets/${answers.name}/${channel}`; 45 | let channel_file_content = channel_templates[answers.language]; 46 | create_file(channel_folder_path, file_name, channel_file_content); 47 | }); 48 | } 49 | } 50 | 51 | const js_connect_template = `exports.process = async ({ connection_id }) => { 52 | console.log('Client connected: ' + connection_id); 53 | // Save the client's connection_id so you can send messages to them later. 54 | }; 55 | `; 56 | 57 | const js_disconnect_template = `exports.process = async ({ connection_id }) => { 58 | console.log('Client disconnected: ' + connection_id); 59 | // Delete the connection_id from your database. 60 | }; 61 | `; 62 | 63 | const py_connect_template = `def process(data): 64 | connection_id = data.get('connection_id') 65 | print(f'Client connected: {connection_id}'); 66 | # Save the client's connection_id so you can send messages to them later. 67 | `; 68 | 69 | const py_disconnect_template = `def process(data): 70 | connection_id = data.get('connection_id') 71 | # Delete the connection_id from your database. 72 | `; 73 | 74 | const connect_templates = { 75 | js: js_connect_template, 76 | ts: socket_templates.load_connect_ts(), 77 | py: py_connect_template 78 | }; 79 | 80 | const disconnect_templates = { 81 | js: js_disconnect_template, 82 | ts: socket_templates.load_disconnect_ts(), 83 | py: py_disconnect_template 84 | }; 85 | 86 | const js_channel_template = `exports.process = async ({ data, connection_id }) => { 87 | console.log(\`Received message from: $\{connection_id\}\`); 88 | console.log(\`Message: $\{data\}\`); 89 | }; 90 | `; 91 | 92 | const py_channel_template = `def process(input_data): 93 | data = input_data.get('data') 94 | connection_id = input_data.get('connection_id') 95 | `; 96 | 97 | const channel_templates = { 98 | js: js_channel_template, 99 | ts: socket_templates.load_channel_ts(), 100 | py: py_channel_template 101 | }; 102 | -------------------------------------------------------------------------------- /commands/create/subscribers/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | inquire_unanswered_questions, 3 | create_file, 4 | language_file_names 5 | } from '../../helpers/index.js'; 6 | 7 | import { topic as topic_templates } from '../../../utils/templates.js'; 8 | 9 | const questions = [ 10 | { 11 | type: 'rawList', 12 | name: 'names', 13 | message: 'Enter a list of Subscribers to create. (E.g. "send_welcome_email send_to_analytics")', 14 | default: 'send_welcome_email send_to_analytics', 15 | validate: function (input) { 16 | if (!/^[a-zA-Z][-a-zA-Z0-9_]*$/.test(input)) { 17 | return 'The Subscribers to the Topic should only contain alphanumeric characters, "-", or "_"'; 18 | } 19 | return true; 20 | }, 21 | }, 22 | { 23 | type: 'input', 24 | name: 'topic', 25 | message: 'Enter the name of the Topic to create or to add Subscribers to. (E.g. "user_created")', 26 | default: 'user_created', 27 | validate: function (input) { 28 | if (!/^[a-zA-Z][-a-zA-Z0-9_]*$/.test(input)) { 29 | return 'The Topic name should only contain alphanumeric characters, "-", or "_"'; 30 | } 31 | return true; 32 | }, 33 | }, 34 | { 35 | type: 'list', 36 | name: 'language', 37 | message: "Enter the programming language to use for the code.", 38 | choices: ['js', 'ts', 'py'] 39 | } 40 | ] 41 | 42 | export default async (options) => { 43 | const answers = await inquire_unanswered_questions(options, questions); 44 | 45 | let topic_path; 46 | if (answers.externalTopic) { 47 | topic_path = `less/external_topics/${answers.externalTopic}`; 48 | } 49 | else { 50 | topic_path = 'less/topics'; 51 | } 52 | 53 | answers.names.forEach(subscriber => { 54 | let subscriber_folder_path = `${topic_path}/${answers.topic}/${subscriber}`; 55 | let subscriber_file_content = subscriber_templates[answers.language]; 56 | const file_name = language_file_names[answers.language]; 57 | create_file(subscriber_folder_path, file_name, subscriber_file_content); 58 | }); 59 | } 60 | 61 | const js_subscriber_template = `exports.process = async (message) => { 62 | console.log(\`Processing message: $\{message\}\`); 63 | }; 64 | `; 65 | 66 | const py_subscriber_template = `def process(message): 67 | print(f"Processing message: \{message\}") 68 | `; 69 | 70 | const subscriber_templates = { 71 | js: js_subscriber_template, 72 | ts: topic_templates.load_topic_subscriber_ts(), 73 | py: py_subscriber_template 74 | }; 75 | -------------------------------------------------------------------------------- /commands/create/topic/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | inquire_unanswered_questions, 3 | create_file, 4 | create_folder, 5 | language_file_names 6 | } from '../../helpers/index.js'; 7 | 8 | import { topic as topic_templates } from '../../../utils/templates.js'; 9 | 10 | const questions = [ 11 | { 12 | type: 'input', 13 | name: 'name', 14 | message: 'Enter the name of the Topic to create or to add channels to. (E.g. "user_created")', 15 | default: 'user_created', 16 | validate: function (input) { 17 | if (!/^[a-zA-Z][-a-zA-Z0-9_]*$/.test(input)) { 18 | return 'The Topic name should only contain alphanumeric characters, "-", or "_"'; 19 | } 20 | return true; 21 | }, 22 | }, 23 | { 24 | type: 'list', 25 | name: 'language', 26 | message: "Enter the programming language to use for the code.", 27 | choices: ['js', 'ts', 'py'] 28 | } 29 | ] 30 | 31 | export default async (options) => { 32 | const answers = await inquire_unanswered_questions(options, questions); 33 | 34 | // Create topic subscribers 35 | if (answers.subscribers) { 36 | let topic_path; 37 | if (answers.externalTopic) { 38 | topic_path = `less/external_topics/${answers.externalTopic}`; 39 | } 40 | else { 41 | topic_path = 'less/topics'; 42 | } 43 | 44 | answers.subscribers.forEach(subscriber => { 45 | let subscriber_folder_path = `${topic_path}/${answers.name}/${subscriber}`; 46 | let subscriber_file_content = subscriber_templates[answers.language]; 47 | const file_name = language_file_names[answers.language]; 48 | create_file(subscriber_folder_path, file_name, subscriber_file_content); 49 | }); 50 | } 51 | // Create topic only 52 | else { 53 | const topic_folder_path = `less/topics/${answers.name}`; 54 | create_folder(topic_folder_path); 55 | } 56 | }; 57 | 58 | const js_subscriber_template = `exports.process = async (message) => { 59 | console.log(\`Processing message: $\{message\}\`); 60 | }; 61 | `; 62 | 63 | const py_subscriber_template = `def process(message): 64 | print(f"Processing message: \{message\}") 65 | `; 66 | 67 | const subscriber_templates = { 68 | js: js_subscriber_template, 69 | ts: topic_templates.load_topic_subscriber_ts(), 70 | py: py_subscriber_template 71 | }; 72 | -------------------------------------------------------------------------------- /commands/create_custom_domain/index.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import ora from 'ora'; 3 | import WebSocket from 'ws'; 4 | import { get_less_token, verify_auth_token } from '../helpers/credentials.js'; 5 | import validate_project_name from '../helpers/validations/validate_project_name.js'; 6 | import validate_static_folder_name from '../helpers/validations/validate_static_folder_name.js'; 7 | import validate_static_domain_name from '../helpers/validations/validate_static_domain_name.js'; 8 | import handleError from '../helpers/handle_error.js'; 9 | import create_server_url from '../helpers/create_server_url.js'; 10 | import axios from 'axios'; 11 | 12 | const spinner = ora({ text: '' }); 13 | 14 | const CLI_PREFIX = '[less-cli]'; 15 | 16 | async function _create_custom_domain({ 17 | organization_id, 18 | projectName, 19 | staticName, 20 | customDomain 21 | }) { 22 | let connectionId; 23 | 24 | const socket = new WebSocket('wss://less-server.chuva.io'); 25 | await new Promise((resolve) => { 26 | socket.on('open', async () => { }); 27 | 28 | socket.on('message', async (data) => { 29 | const message = JSON.parse(data); 30 | 31 | if (message.event === 'conectionInfo') { 32 | connectionId = message.data?.connectionId; 33 | 34 | const LESS_TOKEN = await get_less_token(); 35 | 36 | try { 37 | const headers = { 38 | Authorization: `Bearer ${LESS_TOKEN}`, 39 | 'connection_id': connectionId, 40 | }; 41 | 42 | const serverUrl = create_server_url(organization_id, 'domains/static'); 43 | const response = await axios.post( 44 | serverUrl, 45 | { 46 | project_name: projectName, 47 | static_name: staticName, 48 | custom_domain: customDomain 49 | }, 50 | { headers } 51 | ); 52 | 53 | console.log(); 54 | console.log(chalk.yellowBright(CLI_PREFIX), chalk.greenBright('\t- NS Records')); 55 | console.table({ ...response.data }); 56 | 57 | spinner.stop(); 58 | socket.close(); 59 | resolve(); 60 | } catch (error) { 61 | spinner.stop(); 62 | handleError('Something went wrong'); 63 | socket.close(); 64 | process.exitCode = 1; // Non-success exit code for failure 65 | resolve(); 66 | } 67 | } 68 | }); 69 | }); 70 | } 71 | 72 | export default async function create_custom_domain({ 73 | projectName, 74 | staticName, 75 | customDomain 76 | }, command) { 77 | spinner.start(`${CLI_PREFIX} Connecting to the Less Server... ⚙️`); 78 | spinner.start(); 79 | const organization_id = command.parent.opts().organization; 80 | try { 81 | verify_auth_token() 82 | validate_project_name(projectName) 83 | validate_static_folder_name(staticName) 84 | validate_static_domain_name(customDomain) 85 | 86 | await _create_custom_domain({ 87 | organization_id, 88 | projectName, 89 | staticName, 90 | customDomain 91 | }); 92 | } catch (error) { 93 | spinner.stop(); 94 | handleError(error.message) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /commands/deploy/index.js: -------------------------------------------------------------------------------- 1 | import AdmZip from 'adm-zip'; 2 | import axios from 'axios'; 3 | import chalk from 'chalk'; 4 | import FormData from 'form-data'; 5 | import fs from 'fs'; 6 | import * as glob from 'glob'; 7 | import ora from 'ora'; 8 | import path from 'path'; 9 | import WebSocket from 'ws'; 10 | import yaml from 'js-yaml'; 11 | 12 | import { get_less_token, verify_auth_token } from '../helpers/credentials.js'; 13 | import validate_project_name from '../helpers/validations/validate_project_name.js'; 14 | import handleError from '../helpers/handle_error.js'; 15 | import create_server_url from '../helpers/create_server_url.js'; 16 | import CONFIG from '../../utils/config.js'; 17 | 18 | const spinner = ora({ text: '' }); 19 | 20 | function loadEnvironmentVariables(configFile, cronsPath) { 21 | if (!fs.existsSync(configFile)) { 22 | if (fs.existsSync(cronsPath)) { 23 | const files = fs.readdirSync(cronsPath); 24 | const dirs = files.filter((file) => 25 | fs.statSync(path.join(cronsPath, file)).isDirectory(), 26 | ); 27 | 28 | if (dirs?.length) { 29 | const missingEnvVars = dirs 30 | .map( 31 | (dir) => 32 | `CRON_${dir 33 | .replace(/[A-Z]/g, (match) => `_${match}`) 34 | .toUpperCase()}`, 35 | ) 36 | .join("\n "); 37 | 38 | console.error( 39 | chalk.redBright( 40 | `\nMust create the less.config file and define the follow environment variables on 'env_vars':\n ${missingEnvVars}`, 41 | ), 42 | ); 43 | process.exitCode = 1; 44 | return ; 45 | } 46 | } 47 | 48 | return {}; 49 | } 50 | 51 | 52 | const configFileContent = fs.readFileSync(configFile, 'utf8'); 53 | const config = yaml.load(configFileContent); 54 | 55 | if (!config?.hasOwnProperty('env_vars')) { 56 | console.error(chalk.redBright("\nKey 'env_vars' not found in the less.config file")); 57 | process.exitCode = 1; 58 | return ; 59 | } 60 | 61 | const keys = config.env_vars; 62 | const envVars = {}; 63 | 64 | // Verifying if the less config file has env vars 65 | if (!keys || !keys?.length) { 66 | console.error(chalk.redBright(`\nEnvironment variables must be defined in 'env_vars' on less.config file`)); 67 | process.exitCode = 1; 68 | return ; 69 | } 70 | 71 | for (const key of keys) { 72 | const value = process.env[key]; 73 | if (value === undefined) { 74 | console.error(chalk.redBright(`\nEnvironment variable '${key}' must be defined`)); 75 | process.exitCode = 1; 76 | return ; 77 | } 78 | envVars[key] = value; 79 | } 80 | 81 | return envVars; 82 | } 83 | 84 | async function deployProject(organization_id, projectPath, projectName, envVars) { 85 | let connectionId; 86 | const tempZipFilename = 'temp_project.zip'; 87 | const zip = new AdmZip(); 88 | 89 | const itemsToZip = glob.sync('{less,requirements.txt,yarn.lock,package.lock,less.config,package.json}', { 90 | cwd: projectPath, 91 | }); 92 | 93 | for (const item of itemsToZip) { 94 | const itemPath = path.join(projectPath, item); 95 | 96 | if (fs.statSync(itemPath).isDirectory()) { 97 | zip.addLocalFolder(itemPath, item); 98 | } else { 99 | zip.addLocalFile(itemPath); 100 | } 101 | } 102 | 103 | await zip.writeZipPromise(tempZipFilename); 104 | 105 | const socket = new WebSocket(CONFIG.LESS_SERVER_SOCKET_URL); 106 | 107 | await new Promise((resolve) => { 108 | socket.on('open', async () => { }); 109 | 110 | socket.on('message', async (data) => { 111 | const message = JSON.parse(data); 112 | 113 | if (message.event === 'deploymentStatus') { 114 | const statusData = message.data; 115 | const { status, resources, error } = statusData; 116 | 117 | if (status?.includes('Building...')) { 118 | spinner.stop(); 119 | } 120 | 121 | console.log(chalk.yellowBright('[less-cli]'), chalk.greenBright(status)); 122 | 123 | if (status?.includes('Deploy completed')) { 124 | console.log(chalk.yellowBright('[less-cli]'), '🇨🇻'); 125 | } 126 | 127 | if (resources) { 128 | const { apis, websockets } = resources; 129 | 130 | if (apis?.length) { 131 | console.log(chalk.yellowBright('[less-cli]'), chalk.greenBright('\t- API URLs')); 132 | apis.forEach(api => { 133 | console.log(chalk.yellowBright('[less-cli]'), chalk.greenBright(`\t\t- ${api.api_name}: ${api.url}`)); 134 | }); 135 | } 136 | 137 | if (websockets?.length) { 138 | console.log(chalk.yellowBright('[less-cli]'), chalk.greenBright('\t- WEBSOCKET URLs')); 139 | websockets.forEach(websocket => { 140 | console.log(chalk.yellowBright('[less-cli]'), chalk.greenBright(`\t\t- ${websocket.api_name}: ${websocket.url}`)); 141 | }); 142 | } 143 | 144 | socket.close(); 145 | resolve(); 146 | return ; 147 | } 148 | 149 | if (error) { 150 | socket.close(); 151 | process.exitCode = 1; // Non-success exit code for failure 152 | resolve(); 153 | return ; 154 | } 155 | } 156 | 157 | if (message.event === 'conectionInfo') { 158 | connectionId = message.data?.connectionId; 159 | try { 160 | const formData = new FormData(); 161 | formData.append('zipFile', fs.createReadStream(tempZipFilename)); 162 | formData.append('env_vars', JSON.stringify(envVars)); 163 | formData.append('project_name', JSON.stringify(projectName)); 164 | 165 | const LESS_TOKEN = await get_less_token(); 166 | 167 | const headers = { 168 | Authorization: `Bearer ${LESS_TOKEN}`, 169 | 'connection_id': connectionId, 170 | ...formData.getHeaders(), 171 | }; 172 | 173 | const less_deployment_route = create_server_url(organization_id, 'deploys'); 174 | const response = await axios.post(less_deployment_route, formData, { headers }); 175 | 176 | if (response.status === 202) { 177 | return response.data; 178 | } 179 | } catch (error) { 180 | spinner.stop(); 181 | console.error(chalk.redBright('Error:'), error?.response?.data?.error || 'Deployment failed'); 182 | socket.close(); 183 | process.exitCode = 1; // Non-success exit code for failure 184 | } finally { 185 | if (process.exitCode && process.exitCode !== 0) { 186 | return ; 187 | } 188 | fs.unlinkSync(tempZipFilename); 189 | } 190 | } 191 | }); 192 | }); 193 | } 194 | 195 | export default async function deploy(organization_id, projectName) { 196 | spinner.start('[less-cli] Connecting to the Less Server... ⚙️'); 197 | spinner.start(); 198 | try { 199 | const currentWorkingDirectory = process.cwd(); 200 | const configFile = path.join(currentWorkingDirectory, 'less.config'); 201 | const cronsDir = path.join(currentWorkingDirectory, 'less', 'crons'); 202 | const envVars = loadEnvironmentVariables(configFile, cronsDir); 203 | 204 | if (!envVars) { 205 | return; 206 | } 207 | 208 | verify_auth_token() 209 | validate_project_name(projectName) 210 | 211 | await deployProject(organization_id, currentWorkingDirectory, projectName, envVars); 212 | } catch (error) { 213 | spinner.stop(); 214 | handleError(error.message) 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /commands/deploy_static/index.js: -------------------------------------------------------------------------------- 1 | import AdmZip from 'adm-zip'; 2 | import axios from 'axios'; 3 | import chalk from 'chalk'; 4 | import FormData from 'form-data'; 5 | import fs from 'fs'; 6 | import * as glob from 'glob'; 7 | import ora from 'ora'; 8 | import path from 'path'; 9 | import WebSocket from 'ws'; 10 | 11 | import { get_less_token, verify_auth_token } from '../helpers/credentials.js'; 12 | import handleError from '../helpers/handle_error.js'; 13 | import validate_project_name from '../helpers/validations/validate_project_name.js'; 14 | import create_server_url from '../helpers/create_server_url.js'; 15 | import CONFIG from '../../utils/config.js'; 16 | 17 | const spinner = ora({ text: '' }); 18 | 19 | const CLI_PREFIX = '[less-cli]'; 20 | 21 | async function deployProject(organization_id, projectPath, projectName, envVars) { 22 | let connectionId; 23 | const tempZipFilename = 'temp_project.zip'; 24 | const zip = new AdmZip(); 25 | 26 | const itemsToZip = glob.sync('less/statics/*', { 27 | cwd: projectPath, 28 | }); 29 | 30 | for (const item of itemsToZip) { 31 | const itemPath = path.join(projectPath, item); 32 | 33 | if (fs.statSync(itemPath).isDirectory()) { 34 | zip.addLocalFolder(itemPath, item); 35 | } else { 36 | zip.addLocalFile(itemPath); 37 | } 38 | } 39 | 40 | await zip.writeZipPromise(tempZipFilename); 41 | 42 | const socket = new WebSocket(CONFIG.LESS_SERVER_SOCKET_URL); 43 | 44 | await new Promise((resolve) =>{ 45 | socket.on('open', async () => { }); 46 | 47 | socket.on('message', async (data) => { 48 | const message = JSON.parse(data); 49 | 50 | if (message.event === 'deploymentStatus') { 51 | const statusData = message.data; 52 | const { status, resources, error } = statusData; 53 | 54 | if (status?.includes('Building...')) { 55 | spinner.stop(); 56 | } 57 | 58 | console.log(chalk.yellowBright(CLI_PREFIX), chalk.greenBright(status)); 59 | 60 | if (status?.includes('Deploy completed')) { 61 | console.log(chalk.yellowBright(CLI_PREFIX), '🇨🇻'); 62 | } 63 | 64 | if (resources) { 65 | const { websites } = resources; 66 | 67 | if (websites?.length) { 68 | console.log(chalk.yellowBright(CLI_PREFIX), chalk.greenBright('\t- Websites URLs')); 69 | websites.forEach(website => { 70 | console.log(chalk.yellowBright(CLI_PREFIX), chalk.greenBright(`\t\t- ${website}`)); 71 | }); 72 | } 73 | 74 | socket.close(); 75 | resolve(); 76 | return ; 77 | } 78 | 79 | if (error) { 80 | socket.close(); 81 | process.exitCode = 1; // Non-success exit code for failure 82 | resolve(); 83 | return ; 84 | } 85 | } 86 | 87 | if (message.event === 'conectionInfo') { 88 | connectionId = message.data?.connectionId; 89 | try { 90 | const formData = new FormData(); 91 | formData.append('zipFile', fs.createReadStream(tempZipFilename)); 92 | formData.append('env_vars', JSON.stringify(envVars)); 93 | formData.append('project_name', JSON.stringify(projectName)); 94 | 95 | const LESS_TOKEN = await get_less_token(); 96 | 97 | const headers = { 98 | Authorization: `Bearer ${LESS_TOKEN}`, 99 | 'connection_id': connectionId, 100 | ...formData.getHeaders(), 101 | }; 102 | 103 | const less_static_deployment_route = create_server_url(organization_id, 'deploy-statics'); 104 | const response = await axios.post(less_static_deployment_route, formData, { headers }); 105 | 106 | if (response.status === 202) { 107 | return response.data; 108 | } 109 | } catch (error) { 110 | spinner.stop(); 111 | socket.close(); 112 | resolve(); 113 | handleError('Deployment failed') 114 | } finally { 115 | if (process.exitCode && process.exitCode !== 0) { 116 | resolve(); 117 | return ; 118 | } 119 | fs.unlinkSync(tempZipFilename); 120 | } 121 | } 122 | }); 123 | }); 124 | } 125 | 126 | export default async function deploy(organization_id, projectName) { 127 | spinner.start(`${CLI_PREFIX} Connecting to the Less Server... ⚙️`); 128 | spinner.start(); 129 | try { 130 | const currentWorkingDirectory = process.cwd(); 131 | verify_auth_token(); 132 | validate_project_name(projectName); 133 | 134 | await deployProject(organization_id, currentWorkingDirectory, projectName, {}); 135 | } catch (error) { 136 | spinner.stop(); 137 | handleError(error.message) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /commands/helpers/create_api_url.js: -------------------------------------------------------------------------------- 1 | import config from '../../utils/config.js'; 2 | 3 | const create_api_url = (organization_id, url) => { 4 | return `${config.LESS_API_BASE_URL}/v1/${organization_id ? `organizations/${organization_id}/` : ''}${url}`; 5 | }; 6 | 7 | export default create_api_url; -------------------------------------------------------------------------------- /commands/helpers/create_server_url.js: -------------------------------------------------------------------------------- 1 | import config from '../../utils/config.js'; 2 | 3 | const create_server_url = (organization_id, url) => { 4 | return `${config.LESS_SERVER_BASE_URL}/v1/${organization_id ? `organizations/${organization_id}/` : ''}${url}`; 5 | }; 6 | 7 | export default create_server_url; -------------------------------------------------------------------------------- /commands/helpers/credentials.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises'; 2 | import os from 'os'; 3 | import path from 'path'; 4 | import chalk from 'chalk'; 5 | 6 | const token_error_message = '`LESS_TOKEN` not found in your environment variables. Please login using the `login` command and try again.'; 7 | 8 | // Define the path to the credentials file 9 | const credentials_path = path.join(os.homedir(), '.less-cli', 'credentials'); 10 | 11 | /** 12 | * Set credentials in a JSON file. 13 | * @param {object} credentials - The credentials to be stored in the file. 14 | * @throws {Error} If an error occurs while writing to the file. 15 | */ 16 | export async function set_credentials(credentials) { 17 | try { 18 | // Ensure the directory exists 19 | await fs.mkdir(path.dirname(credentials_path), { recursive: true }); 20 | 21 | // Write the credentials to the file 22 | await fs.writeFile(credentials_path, JSON.stringify(credentials), 'utf8'); 23 | } catch (err) { 24 | console.error(chalk.redBright('Error:'), `Error writing to ${credentials_path}:`, err); 25 | process.exitCode = 1; 26 | } 27 | } 28 | 29 | /** 30 | * Get stored credentials from a JSON file. 31 | * @returns {object} The stored credentials. 32 | * @throws {Error} If the file doesn't exist or an error occurs while reading from the file. 33 | */ 34 | export async function get_credentials() { 35 | try { 36 | // Check if the file exists 37 | await fs.access(credentials_path); 38 | 39 | // If the file exists, read from it 40 | const data = await fs.readFile(credentials_path, 'utf8'); 41 | 42 | // Parse the credentials 43 | const credentials = JSON.parse(data); 44 | 45 | return credentials; 46 | } catch (err) { 47 | console.error(chalk.redBright('Error:'), token_error_message); 48 | process.exitCode = 1; 49 | } 50 | } 51 | 52 | /** 53 | * Get a LESS token from environment variables or stored credentials. 54 | * @returns {string} The LESS token. 55 | * @throws {Error} If the file doesn't exist or an error occurs while reading from the file. 56 | */ 57 | export async function get_less_token() { 58 | try { 59 | let token = process.env.LESS_TOKEN; 60 | 61 | if (token) { 62 | return token; 63 | } 64 | 65 | const credentials = await get_credentials(); 66 | 67 | if (!credentials?.LESS_TOKEN) { 68 | console.error(chalk.redBright('Error:'), token_error_message); 69 | process.exitCode = 1; 70 | } 71 | 72 | return credentials?.LESS_TOKEN; 73 | } catch (err) { 74 | console.error(chalk.redBright('Error:'), token_error_message); 75 | process.exitCode = 1; 76 | } 77 | } 78 | 79 | /** 80 | * Verify the presence of an authentication token in environment variables or stored credentials. 81 | * If no token is found, it displays an error message and exits the process. 82 | * @throws {Error} If the token is not found in environment variables or stored credentials. 83 | */ 84 | export async function verify_auth_token() { 85 | if (!process.env.LESS_TOKEN) { 86 | const credentials = await get_credentials(); 87 | 88 | if (!credentials?.LESS_TOKEN) { 89 | console.error(chalk.redBright('Error:'), '`LESS_TOKEN` not found in your environment variables. Please login using the `login` command and try again.'); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /commands/helpers/handle_error.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | // Helper function to handle errors 4 | export default function handleError(message) { 5 | console.error(chalk.redBright('Error:'), message || 'An error occurred'); 6 | process.exitCode = 1; 7 | } 8 | -------------------------------------------------------------------------------- /commands/helpers/index.js: -------------------------------------------------------------------------------- 1 | import inquirer from 'inquirer'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import chalk from 'chalk'; 5 | 6 | const filter_unanswered_questions = (options, questions) => { 7 | const options_set = new Set(Object.keys(options)); 8 | return questions.filter(question => !options_set.has(question.name)); 9 | }; 10 | 11 | const inquire_unanswered_questions = async (options, questions) => { 12 | const unanswered_questions = filter_unanswered_questions(options, questions); 13 | const answers = await inquirer.prompt(unanswered_questions); 14 | return { ...options, ...answers }; // Return all answers 15 | }; 16 | 17 | const create_file = (folder_path, file_name, file_content = '') => { 18 | // Create the route directory if it doesn't exist 19 | if (!fs.existsSync(folder_path)) { 20 | fs.mkdirSync(folder_path, { recursive: true }); 21 | } 22 | 23 | // Create the file if it doesn't exist 24 | const file_path = path.join(folder_path, file_name); 25 | if (!fs.existsSync(file_path)) { 26 | fs.writeFileSync(file_path, file_content); 27 | console.log(chalk.green('File created:'), `${file_path}`); 28 | } else { 29 | console.log(chalk.yellow('File already exists:'), `${file_path}`); 30 | } 31 | }; 32 | 33 | const create_folder = (folder_path) => { 34 | // Create the route directory if it doesn't exist 35 | if (!fs.existsSync(folder_path)) { 36 | fs.mkdirSync(folder_path, { recursive: true }); 37 | console.log(chalk.green('Folder created:'), `${folder_path}`); 38 | } else { 39 | console.log(chalk.yellow('Folder already exists:'), `${folder_path}`); 40 | } 41 | }; 42 | 43 | const language_file_names = { 44 | js: 'index.js', 45 | ts: 'index.ts', 46 | py: '__init__.py' 47 | }; 48 | 49 | export { 50 | inquire_unanswered_questions, 51 | create_file, 52 | create_folder, 53 | language_file_names 54 | }; 55 | -------------------------------------------------------------------------------- /commands/helpers/validations/validate_project_folder.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | const validate_project_dir = (project_path) => { 4 | const less_logic_path = path.join(project_path, 'less') 5 | if (!fs.existsSync(less_logic_path)) { 6 | throw new Error(`The folder ${project_path} is not a less project`); 7 | } 8 | } 9 | 10 | export default validate_project_dir; -------------------------------------------------------------------------------- /commands/helpers/validations/validate_project_name.js: -------------------------------------------------------------------------------- 1 | export default function validate_project_name(project_name) { 2 | if (!/^[a-z][-a-z0-9]*$/.test(project_name)) { 3 | throw new Error('The project_name must satisfy regular expression pattern: [a-z][-a-z0-9]'); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /commands/helpers/validations/validate_project_structure/errors/index.js: -------------------------------------------------------------------------------- 1 | export class ResourceNameInvalidException extends Error { 2 | constructor(message) { 3 | super(message); 4 | this.name = 'ResourceNameInvalidException'; 5 | } 6 | }; 7 | 8 | export class ResourceHandlerNotFoundException extends Error { 9 | constructor(message) { 10 | super(message); 11 | this.name = 'ResourceHandlerNotFoundException'; 12 | } 13 | }; -------------------------------------------------------------------------------- /commands/helpers/validations/validate_project_structure/index.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | 4 | import validate_apis from './validate_apis/index.js'; 5 | import validate_crons from './validate_crons/index.js'; 6 | import validate_shared from './validate_shared/index.js'; 7 | import validate_topics from './validate_topics/index.js'; 8 | import validate_sockets from './validate_sockets/index.js'; 9 | import validate_cloud_functions from './validate_cloud_functions/index.js'; 10 | import validate_external_topics from './validate_external_topics/index.js'; 11 | import { ResourceHandlerNotFoundException, ResourceNameInvalidException } from './errors/index.js'; 12 | 13 | export default (project_path) => { 14 | const project_less_path = path.join(project_path, 'less'); 15 | const resources_types = ['apis', 'crons', 'shared', 'topics', 'sockets', 'functions', 'external_topics', 'statics']; 16 | 17 | const less_allowed_folders_message = 18 | `The "less" folder should only contain folders that match the following:${ 19 | resources_types.map(resource_type => `\n\t- ${resource_type}`).join('') 20 | }`; 21 | 22 | let resources_found = false; 23 | fs.readdirSync(project_less_path).forEach(resource_type => { 24 | const resource_path = path.join(project_less_path, resource_type); 25 | 26 | if ( 27 | !fs.statSync(resource_path).isDirectory() 28 | || !resources_types.includes(resource_type) 29 | ) { 30 | throw new ResourceNameInvalidException(`Invalid resource type "${resource_type}" in the "less" folder.\n${less_allowed_folders_message}`); 31 | } 32 | 33 | const resources_exists = ( 34 | fs.existsSync(resource_path) 35 | && fs 36 | .readdirSync(resource_path) 37 | .filter(element => fs.statSync(path.join(resource_path, element)).isDirectory()) 38 | .length 39 | ); 40 | 41 | if (resources_exists) { 42 | resources_found = true; 43 | } 44 | }); 45 | 46 | if (!resources_found) { 47 | throw new ResourceHandlerNotFoundException(`No resources found in the "less" folder. ${less_allowed_folders_message}`); 48 | } 49 | 50 | validate_apis(project_less_path); 51 | validate_crons(project_less_path); 52 | validate_shared(project_less_path); 53 | validate_topics(project_less_path); 54 | validate_sockets(project_less_path); 55 | validate_cloud_functions(project_less_path); 56 | validate_external_topics(project_less_path); 57 | } 58 | -------------------------------------------------------------------------------- /commands/helpers/validations/validate_project_structure/validate_apis/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | import validate_api_route_path from './validate_api_routes/index.js'; 5 | import { ResourceNameInvalidException } from '../errors/index.js'; 6 | import validate_resource_instance_name from '../../validate_resources_instances_names.js'; 7 | 8 | export default (project_less_path) => { 9 | const apis_path = path.join(project_less_path, 'apis'); 10 | 11 | if (!fs.existsSync(apis_path)) { 12 | return; 13 | } 14 | 15 | const apis = fs.readdirSync(apis_path); 16 | 17 | if (!apis.length) { 18 | return; 19 | } 20 | 21 | apis.forEach((api) => { 22 | if (!validate_resource_instance_name(api)) { 23 | throw new ResourceNameInvalidException( 24 | `Invalid API name "${api}". ${validate_resource_instance_name.regexConstrainMessage}.` 25 | ); 26 | } 27 | 28 | validate_api_route_path(api); 29 | }); 30 | } -------------------------------------------------------------------------------- /commands/helpers/validations/validate_project_structure/validate_apis/validate_api_routes/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { ResourceNameInvalidException, ResourceHandlerNotFoundException } from '../../errors/index.js'; 4 | import validate_resource_instance_name from '../../../validate_resources_instances_names.js'; 5 | 6 | const http_method_extensions = ['js', 'ts', 'py', 'rs']; 7 | const http_methods = ['get', 'post', 'put', 'patch', 'delete']; 8 | 9 | const validate_route_method_handler = (method_handler_name) => 10 | RegExp(`^(${http_methods.join('|')})\.(${http_method_extensions.join('|')})$`).test(method_handler_name); 11 | 12 | const validate_api_routes = (api, current_route = '') => { 13 | const current_route_path = path.join(process.cwd(), 'less/apis', api, current_route); 14 | const directory_items = fs.readdirSync(current_route_path); 15 | 16 | const path_segments = directory_items.filter( 17 | (item) => fs.statSync(path.join(current_route_path, item)).isDirectory() 18 | ); 19 | const http_method_handlers = directory_items.filter( 20 | (method_handler) => fs.statSync(path.join(current_route_path, method_handler)).isFile() 21 | && validate_route_method_handler(method_handler) 22 | ); 23 | 24 | path_segments.forEach((path_segment) => { 25 | const route = path.join(current_route, path_segment); 26 | 27 | const cleaned_path_segment = path_segment.replace('{', '').replace('}', ''); 28 | if (!validate_resource_instance_name(cleaned_path_segment)) { 29 | throw new ResourceNameInvalidException( 30 | `Invalid path segment "${path_segment}" from route "${route}" on API "${api}". ${validate_resource_instance_name.regexConstrainMessage}, and can be wrapped by curly braces to be treated as a dynamic path segment.` 31 | ); 32 | } 33 | 34 | validate_api_routes(api, route); 35 | }); 36 | 37 | if (!path_segments.length && !http_method_handlers.length) { 38 | throw new ResourceHandlerNotFoundException( 39 | `No method handlers files found on route "${current_route}" from API "${api}". 40 | Should contain one of the following:${http_methods.map(method => `\n\t- ${method}.(${http_method_extensions.join('|')})`).join('')}` 41 | ); 42 | } 43 | }; 44 | 45 | export default validate_api_routes; 46 | -------------------------------------------------------------------------------- /commands/helpers/validations/validate_project_structure/validate_cloud_functions/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | import validate_resource_instance_name from '../../validate_resources_instances_names.js'; 5 | import { ResourceNameInvalidException, ResourceHandlerNotFoundException } from '../errors/index.js'; 6 | 7 | export default (project_less_path) => { 8 | const cloud_functions_path = path.join(project_less_path, 'functions'); 9 | 10 | if (!fs.existsSync(cloud_functions_path)) { 11 | return; 12 | } 13 | 14 | const cloud_functions = fs.readdirSync(cloud_functions_path) 15 | .filter((element) => fs.statSync(path.join(cloud_functions_path, element)).isDirectory()); 16 | 17 | if (!cloud_functions.length) { 18 | return; 19 | } 20 | 21 | cloud_functions.forEach((cloud_function) => { 22 | if (!validate_resource_instance_name(cloud_function)) { 23 | throw new ResourceNameInvalidException( 24 | `Invalid cloud function name "${cloud_function}". ${validate_resource_instance_name.regexConstrainMessage}` 25 | ); 26 | } 27 | 28 | const cloud_function_items = fs.readdirSync( 29 | path.join(cloud_functions_path, cloud_function) 30 | ); 31 | 32 | if ( 33 | !cloud_function_items.length 34 | || !cloud_function_items.find((item) => ( 35 | fs.statSync(path.join(cloud_functions_path, cloud_function, item)).isFile() 36 | && /^(index\.js|index\.ts|__init__\.py)$/.test(item) 37 | )) 38 | ) { 39 | throw new ResourceHandlerNotFoundException( 40 | `Cloud function "${cloud_function}" doesn't have a handler file named "index.js", "index.ts", or "__init__.py".` 41 | ); 42 | } 43 | }); 44 | } -------------------------------------------------------------------------------- /commands/helpers/validations/validate_project_structure/validate_crons/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | import validate_resource_instance_name from '../../validate_resources_instances_names.js'; 5 | import { ResourceNameInvalidException, ResourceHandlerNotFoundException } from '../errors/index.js'; 6 | 7 | export default (project_less_path) => { 8 | const crons_path = path.join(project_less_path, 'crons'); 9 | 10 | if (!fs.existsSync(crons_path)) { 11 | return; 12 | } 13 | 14 | const crons = fs.readdirSync(crons_path) 15 | .filter((element) => fs.statSync(path.join(crons_path, element)).isDirectory()); 16 | 17 | if (!crons.length) { 18 | return; 19 | } 20 | 21 | crons.forEach((cron) => { 22 | if (!validate_resource_instance_name(cron)) { 23 | throw new ResourceNameInvalidException( 24 | `Invalid cron name "${cron}". ${validate_resource_instance_name.regexConstrainMessage}` 25 | ); 26 | } 27 | 28 | const cron_items = fs.readdirSync(path.join(crons_path, cron)); 29 | 30 | if ( 31 | !cron_items.length 32 | || !cron_items.find((item) => ( 33 | fs.statSync(path.join(crons_path, cron, item)).isFile() 34 | && /^(index\.js|index\.ts|__init__\.py)$/.test(item) 35 | )) 36 | ) { 37 | throw new ResourceHandlerNotFoundException( 38 | `Cron "${cron}" doesn't have a handler file named "index.js", "index.ts", or "__init__.py".` 39 | ); 40 | } 41 | }); 42 | } -------------------------------------------------------------------------------- /commands/helpers/validations/validate_project_structure/validate_external_topics/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | import validate_resource_instance_name from '../../validate_resources_instances_names.js'; 5 | import { ResourceNameInvalidException, ResourceHandlerNotFoundException } from '../errors/index.js'; 6 | 7 | export default (project_less_path) => { 8 | const external_topics_projects_path = path.join(project_less_path, 'external_topics'); 9 | 10 | if (!fs.existsSync(external_topics_projects_path)) { 11 | return; 12 | } 13 | 14 | const external_topics_projects_placeholders = fs 15 | .readdirSync(external_topics_projects_path) 16 | .filter((element) => fs 17 | .statSync(path.join(external_topics_projects_path, element)) 18 | .isDirectory() 19 | ); 20 | 21 | if (!external_topics_projects_placeholders.length) { 22 | return; 23 | } 24 | 25 | external_topics_projects_placeholders.forEach((external_topics_project_placeholder) => { 26 | if (!validate_resource_instance_name(external_topics_project_placeholder)) { 27 | throw new ResourceNameInvalidException( 28 | `Invalid project placeholder name "${external_topics_project_placeholder}" for external topics. 29 | ${validate_resource_instance_name.regexConstrainMessage}.` 30 | ); 31 | } 32 | 33 | const external_project_topics_path = path.join( 34 | external_topics_projects_path, 35 | external_topics_project_placeholder 36 | ); 37 | 38 | const topics = fs 39 | .readdirSync(external_project_topics_path) 40 | .filter(element => fs 41 | .statSync(path.join(external_project_topics_path, element)) 42 | .isDirectory() 43 | ); 44 | 45 | if (!topics.length) { 46 | throw new ResourceHandlerNotFoundException( 47 | `external project "${external_topics_project_placeholder}" located at "less/external_topics", should contain at least one topic.` 48 | ); 49 | } 50 | 51 | topics.forEach(topic => { 52 | if (!validate_resource_instance_name(topic)) { 53 | throw new ResourceNameInvalidException( 54 | `Invalid external topic name "${topic}" located on "less/external_topics/${external_topics_project_placeholder}". 55 | ${validate_resource_instance_name.regexConstrainMessage}` 56 | ); 57 | } 58 | 59 | const topic_path = path.join(external_project_topics_path, topic); 60 | const topic_processors = fs 61 | .readdirSync(topic_path) 62 | .filter(element => fs 63 | .statSync(path.join(topic_path, element)) 64 | .isDirectory() 65 | ); 66 | 67 | if (!topic_processors.length) { 68 | throw new ResourceHandlerNotFoundException( 69 | `External topic "${topic}" from external project placholder "${external_topics_project_placeholder}" should contain at least one processor.` 70 | ); 71 | } 72 | 73 | topic_processors.forEach(processor => { 74 | if (!validate_resource_instance_name(processor)) { 75 | throw new ResourceNameInvalidException( 76 | `Invalid processor name "${processor}" from external topic "${topic}" located at "less/external_topics/${external_topics_project_placeholder}". 77 | ${validate_resource_instance_name.regexConstrainMessage}` 78 | ); 79 | } 80 | 81 | const processor_path = path.join(topic_path, processor); 82 | const processor_handler = fs 83 | .readdirSync(processor_path) 84 | .filter(element => 85 | fs.statSync(path.join(processor_path, element)).isFile() 86 | && /^(index\.js|index\.ts|__init__\.py)$/.test(element) 87 | ); 88 | 89 | if (!processor_handler.length) { 90 | throw new ResourceHandlerNotFoundException( 91 | `Processor "${processor}" from external topic "${topic}" located at "less/external_topics/${external_topics_project_placeholder}, doesn't have a handler file named "index.js", "index.ts", or "__init__.py".` 92 | ); 93 | } 94 | }) 95 | }); 96 | }); 97 | } -------------------------------------------------------------------------------- /commands/helpers/validations/validate_project_structure/validate_shared/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | export default (project_less_path) => { 5 | const shared_path = path.join(project_less_path, 'shared'); 6 | 7 | if (!fs.existsSync(shared_path)) { 8 | return; 9 | } 10 | 11 | const shareds = fs.readdirSync(shared_path) 12 | 13 | if (!shareds.length) { 14 | return; 15 | } 16 | 17 | if (shareds.find(elemet => fs.statSync(path.join(shared_path, elemet)).isFile())) { 18 | throw new Error('The shared folder "less/shared" should only contain directories.'); 19 | } 20 | } -------------------------------------------------------------------------------- /commands/helpers/validations/validate_project_structure/validate_sockets/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import chalk from 'chalk'; 4 | 5 | import validate_resource_instance_name from '../../validate_resources_instances_names.js'; 6 | import { ResourceNameInvalidException, ResourceHandlerNotFoundException } from '../errors/index.js'; 7 | 8 | export default (project_less_path) => { 9 | const sockets_path = path.join(project_less_path, 'sockets'); 10 | 11 | if (!fs.existsSync(sockets_path)) { 12 | return; 13 | } 14 | 15 | const sockets = fs 16 | .readdirSync(sockets_path) 17 | .filter(element => fs 18 | .statSync(path.join(sockets_path, element)) 19 | .isDirectory() 20 | ); 21 | 22 | if (!sockets.length) { 23 | return; 24 | } 25 | 26 | sockets.forEach(socket => { 27 | if (!validate_resource_instance_name(socket)) { 28 | throw new ResourceNameInvalidException( 29 | `Invalid socket name "${socket}". ${validate_resource_instance_name.regexConstrainMessage}.` 30 | ); 31 | } 32 | 33 | const socket_path = path.join(sockets_path, socket); 34 | const socket_channels = fs 35 | .readdirSync(socket_path) 36 | .filter(element => fs 37 | .statSync(path.join(socket_path, element)) 38 | .isDirectory() 39 | ); 40 | 41 | const connections_handlers = ['connect', 'disconnect']; 42 | const connections_handlers_filtered = connections_handlers 43 | .filter(handler => !socket_channels.includes(handler)); 44 | 45 | if (connections_handlers_filtered.length) { 46 | console.log(chalk.yellowBright( 47 | `WARNING: Socket "${socket}" does not contain the ${connections_handlers_filtered.join(' and ')} ${connections_handlers_filtered.length === 2 ? 'handlers' : 'handler'}.` 48 | )) 49 | } 50 | 51 | socket_channels.forEach(channel => { 52 | if (!validate_resource_instance_name(channel)) { 53 | throw new ResourceNameInvalidException( 54 | `Invalid channel name "${channel}" from socket "${socket}". ${validate_resource_instance_name.regexConstrainMessage}.` 55 | ); 56 | } 57 | 58 | const channel_path = path.join(socket_path, channel); 59 | const channel_handler = fs 60 | .readdirSync(channel_path) 61 | .filter(element => 62 | fs.statSync(path.join(channel_path, element)).isFile() 63 | && /^(index\.js|index\.ts|__init__\.py)$/.test(element) 64 | ); 65 | 66 | if (!channel_handler.length) { 67 | throw new ResourceHandlerNotFoundException( 68 | `The ${connections_handlers.includes(channel) ? `${channel} handler`: `channel "${channel}"`} from socket "${socket}", doesn't have a handler file named "index.js", "index.ts", or "__init__.py".` 69 | ); 70 | } 71 | }) 72 | }); 73 | } -------------------------------------------------------------------------------- /commands/helpers/validations/validate_project_structure/validate_topics/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | import validate_resource_instance_name from '../../validate_resources_instances_names.js'; 5 | import { ResourceNameInvalidException, ResourceHandlerNotFoundException } from '../errors/index.js'; 6 | 7 | export default (project_less_path) => { 8 | const topics_path = path.join(project_less_path, 'topics'); 9 | 10 | if (!fs.existsSync(topics_path)) { 11 | return; 12 | } 13 | 14 | const topics = fs 15 | .readdirSync(topics_path) 16 | .filter(element => fs 17 | .statSync(path.join(topics_path, element)) 18 | .isDirectory() 19 | ); 20 | 21 | if (!topics.length) { 22 | return; 23 | } 24 | 25 | topics.forEach(topic => { 26 | if (!validate_resource_instance_name(topic)) { 27 | throw new ResourceNameInvalidException( 28 | `Invalid Topic name "${topic}". ${validate_resource_instance_name.regexConstrainMessage}.` 29 | ); 30 | } 31 | 32 | const topic_path = path.join(topics_path, topic); 33 | const topic_processors = fs 34 | .readdirSync(topic_path) 35 | .filter(element => fs 36 | .statSync(path.join(topic_path, element)) 37 | .isDirectory() 38 | ); 39 | 40 | if (!topic_processors.length) { 41 | throw new Error( 42 | `Topic "${topic}" should contain at least one processor.` 43 | ); 44 | } 45 | 46 | topic_processors.forEach(processor => { 47 | if (!validate_resource_instance_name(processor)) { 48 | throw new ResourceNameInvalidException( 49 | `Invalid processor name "${processor}" from topic "${topic}". ${validate_resource_instance_name.regexConstrainMessage}.` 50 | ); 51 | } 52 | 53 | const processor_path = path.join(topic_path, processor); 54 | const processor_handler = fs 55 | .readdirSync(processor_path) 56 | .filter(element => 57 | fs.statSync(path.join(processor_path, element)).isFile() 58 | && /^(index\.js|index\.ts|__init__\.py)$/.test(element) 59 | ); 60 | 61 | if (!processor_handler.length) { 62 | throw new ResourceHandlerNotFoundException( 63 | `Processor "${processor}" from topic "${topic}", doesn't have a handler file named "index.js", "index.ts", or "__init__.py".` 64 | ); 65 | } 66 | }) 67 | }); 68 | } -------------------------------------------------------------------------------- /commands/helpers/validations/validate_resources_instances_names.js: -------------------------------------------------------------------------------- 1 | const validate_resource_instance_name = (resource_instance_name) => /^[A-Za-z][A-Za-z0-9_]*$/.test(resource_instance_name); 2 | validate_resource_instance_name.regexConstrainMessage = 3 | 'Should start with a letter and contain only letters, numbers and underscores'; 4 | 5 | export default validate_resource_instance_name; -------------------------------------------------------------------------------- /commands/helpers/validations/validate_static_domain_name.js: -------------------------------------------------------------------------------- 1 | export default function validate_static_domain_name(domainName) { 2 | if ( 3 | !domainName || 4 | !/^(?!https?:\/\/)[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(domainName) 5 | ) { 6 | throw new Error('The customDomain must satisfy regular expression pattern: (?!https?:\/\/)[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$') 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /commands/helpers/validations/validate_static_folder_name.js: -------------------------------------------------------------------------------- 1 | export default function validate_static_folder_name(name) { 2 | if (!/^[a-z][-a-z0-9]*$/.test(name)) { 3 | throw new Error('The project_name must satisfy regular expression pattern: [a-z][-a-z0-9]'); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /commands/list_organizations.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import chalk from 'chalk'; 3 | import { verify_auth_token, get_less_token } from './helpers/credentials.js'; 4 | import config from '../utils/config.js'; 5 | import Table from 'cli-table3'; 6 | 7 | export default async function get_all(organization_id) { 8 | await verify_auth_token(); 9 | 10 | const apiUrl = `${config.LESS_API_BASE_URL}/v1/organizations`; 11 | 12 | try { 13 | const LESS_TOKEN = await get_less_token(); 14 | 15 | const headers = { 16 | Authorization: `Bearer ${LESS_TOKEN}` 17 | }; 18 | 19 | const response = await axios.get(apiUrl, { headers }); 20 | if (response.status === 200) { 21 | // Prepare table for CLI output 22 | 23 | // Prepare the table headers 24 | const headers = [ 25 | "Organization ID", 26 | "Organization Name", 27 | "Email", 28 | "Created", 29 | ].map(title => chalk.bold.greenBright(title)); // Set header colors 30 | 31 | const table = new Table({ 32 | head: headers 33 | }); 34 | 35 | // Set table colors for each item 36 | table.push( 37 | ...response.data.data 38 | .map(item => ([ 39 | item.id, 40 | item.name, 41 | item.email, 42 | item.created_at 43 | ] 44 | .map(item => chalk.cyanBright(item)) // Set item colors 45 | )) 46 | ); 47 | 48 | console.log(table.toString()); 49 | } 50 | } catch (error) { 51 | console.error(chalk.redBright('Error:'), error?.response?.data?.error || 'Get projects failed'); 52 | console.error(chalk.redBright('Error:'), error); 53 | process.exitCode = 1; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /commands/local/build/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import ora from 'ora'; 3 | import path from 'path'; 4 | import chalk from 'chalk'; 5 | import { exec } from 'child_process'; 6 | import { randomUUID } from 'crypto'; 7 | 8 | import build_apis from '../helpers/build_apis/index.js'; 9 | import build_crons from '../helpers/build_crons/index.js'; 10 | import build_topics from '../helpers/build_topics/index.js'; 11 | import build_sockets from '../helpers/build_sockets/index.js'; 12 | import build_shared_code from '../helpers/build_shared_code/index.js'; 13 | import { get as get_build_path } from '../helpers/build_path/index.js'; 14 | import add_to_package_json from '../helpers/add_to_package_json/index.js'; 15 | import less_app_config_file from '../helpers/less_app_config_file/index.js'; 16 | import build_cloud_functions from '../helpers/build_cloud_functions/index.js'; 17 | import build_less_dependencies from '../helpers/build_less_dependencies/index.js'; 18 | import build_sqlite_database_dependency from '../helpers/build_sqlite_database_dependency/index.js'; 19 | import create_dotenv_based_on_less_config from '../helpers/create_dotenv_based_on_less_config/index.js'; 20 | import get_yarn_path from '../helpers/get_yarn_path/index.js'; 21 | 22 | import { APP_CONFIG_FILE, LESS_LOCAL_FLAG, LESS_LOCAL_INFO_FLAG, LESS_LOCAL_ERROR_FLAG } from '../constants/index.js' 23 | 24 | const python_handler = `import sys 25 | import json 26 | import argparse 27 | #python-import# 28 | 29 | #python-snipped-code# 30 | 31 | parser = argparse.ArgumentParser() 32 | parser.add_argument('--data') 33 | args = parser.parse_args() 34 | 35 | def handler(data): 36 | #python-function-call# 37 | 38 | response = handler(args.data) 39 | 40 | sys.stdout.write(f'#LESS-EXPRESS::RETURNING-RESPONSE::<{{{json.dumps(response)}}}>::RETURNING-RESPONSE::LESS-EXPRESS#') 41 | `; 42 | 43 | const deploy = async (project_name) => { 44 | try { 45 | const project_location = process.cwd(); 46 | const less_local_flag = chalk.yellowBright(LESS_LOCAL_FLAG); 47 | const spinner = ora(); 48 | 49 | const project_build_path = path.join( 50 | get_build_path(), 51 | project_name 52 | ); 53 | 54 | const less_sqlite_db = 'less-sqlite-db.db'; 55 | const project_build_exists = fs.existsSync(project_build_path); 56 | 57 | if (project_build_exists) { 58 | const build_files_and_folders = fs.readdirSync(project_build_path); 59 | build_files_and_folders.map(item => { 60 | const items_to_spare = [ 61 | 'yarn.lock', 62 | 'node_modules', 63 | less_sqlite_db, 64 | APP_CONFIG_FILE 65 | ]; 66 | 67 | if (items_to_spare.includes(item)) { 68 | return; 69 | } 70 | 71 | const item_path = path.resolve(project_build_path, item); 72 | if (fs.statSync(item_path).isDirectory()) { 73 | fs.rmSync(item_path, { recursive: true }); 74 | } else { 75 | fs.unlinkSync(item_path); 76 | } 77 | }); 78 | } 79 | else { 80 | fs.mkdirSync(project_build_path, { recursive: true }); 81 | } 82 | 83 | // If tsconfig.json exists add it to the build path 84 | const tsconfig_path = path.resolve(project_location, 'tsconfig.json'); 85 | if (fs.existsSync(tsconfig_path)) { 86 | fs.copyFileSync( 87 | tsconfig_path, 88 | path.resolve(project_build_path, 'tsconfig.json') 89 | ); 90 | } 91 | 92 | const project_less_resources_location = path.resolve( 93 | project_location, 94 | 'less' 95 | ); 96 | 97 | const less_resources = { 98 | apis: 'apis', 99 | shared: 'shared', 100 | topics: 'topics', 101 | sockets: 'sockets', 102 | crons: 'crons', 103 | functions: 'functions' 104 | }; 105 | 106 | const chuva_dependency = '@chuva.io/less'; 107 | const less_sqlite_tables = { 108 | topics: { 109 | table: 'topics_processors_queues', 110 | model: 'TopicsProcessorsQueuesDB' 111 | }, 112 | kvs: { 113 | table: 'key_value_storage', 114 | model: 'KeyValueStorageDB' 115 | } 116 | }; 117 | const config = { 118 | apis: {}, 119 | shared: [chuva_dependency], 120 | project_name, 121 | sockets: {}, 122 | less_resources, 123 | python_handler, 124 | less_sqlite_db, 125 | app_imports: '', 126 | app_callers: '', 127 | chuva_dependency, 128 | project_location, 129 | socket_port: 8000, 130 | project_build_path, 131 | less_sqlite_tables, 132 | rest_api_port: 3333, 133 | api_routes: 'routes', 134 | content_paths_to_delete: [], 135 | project_less_resources_location, 136 | sqlite_database_dependency: 'lessSqliteDB', 137 | less_local_info_flag: LESS_LOCAL_INFO_FLAG, 138 | less_local_error_flag: LESS_LOCAL_ERROR_FLAG, 139 | less_websocket_clients: 'lessWebsocketClients', 140 | javascript_dependencies_file_name: 'package.json', 141 | python_handler_dependency: '@chuva.io/execute_python_handler', 142 | app_running_flag: `${randomUUID()}}`, 143 | chuva_dependency_path: path.resolve(project_build_path, chuva_dependency) 144 | }; 145 | 146 | await create_dotenv_based_on_less_config(config); 147 | build_less_dependencies(config); 148 | await build_sqlite_database_dependency(config); 149 | build_shared_code(config); 150 | 151 | spinner.text = `${chalk.gray(`${LESS_LOCAL_FLAG} Building...`)}🚀`; 152 | spinner.start(); 153 | build_topics(config); 154 | build_apis(config); 155 | build_sockets(config); 156 | build_crons(config); 157 | build_cloud_functions(config); 158 | add_to_package_json(config, { 159 | dependencies: { 160 | "cron": "^3.1.7" 161 | } 162 | }); 163 | 164 | fs.writeFileSync( 165 | path.resolve(config.project_build_path, 'app.js'), 166 | `${config.app_imports} 167 | 168 | ${config.app_callers} 169 | console.log(require('./${APP_CONFIG_FILE}').app_running_flag); 170 | ` 171 | ); 172 | 173 | const less_app_config_file_data = less_app_config_file.get(config.project_build_path); 174 | 175 | const updated_at = new Date().toISOString(); 176 | less_app_config_file.set(config.project_build_path, { 177 | apis: config.apis, 178 | sockets: config.sockets, 179 | app_running_flag: config.app_running_flag, 180 | updated_at 181 | }); 182 | 183 | if (!Object.keys(less_app_config_file_data).length) { 184 | less_app_config_file.set(config.project_build_path, { 185 | created_at: updated_at 186 | }); 187 | } 188 | 189 | spinner.stop(); 190 | console.log( 191 | less_local_flag, 192 | chalk.greenBright('Resources built with success ✅') 193 | ); 194 | 195 | spinner.text = `${chalk.gray(`${LESS_LOCAL_FLAG} Installing packages...`)}📦`; 196 | spinner.start(); 197 | 198 | const yarn_path = get_yarn_path(); 199 | await new Promise((resolve, reject) => exec( 200 | `cd ${config.project_build_path} 201 | ${yarn_path} 202 | ${config.shared.length ? `${yarn_path} upgrade ${config.shared.join(' ')}` : ''}`, 203 | (error, stdout, stderr) => { 204 | if (error) { 205 | reject(error.toString('utf-8')); 206 | return; 207 | } 208 | 209 | resolve(); 210 | } 211 | )); 212 | spinner.stop(); 213 | console.log( 214 | less_local_flag, 215 | chalk.greenBright('Packages installed with success ✅') 216 | ); 217 | } catch(error) { 218 | console.log('Error:', error); 219 | process.exitCode = 1; 220 | } 221 | }; 222 | 223 | export default deploy; -------------------------------------------------------------------------------- /commands/local/constants/index.js: -------------------------------------------------------------------------------- 1 | export const APP_CONFIG_FILE = 'less-app-config.json'; 2 | export const LESS_LOCAL_FLAG = '[less-local]'; 3 | export const LESS_LOCAL_INFO_FLAG = ''; 4 | export const LESS_LOCAL_ERROR_FLAG = ''; -------------------------------------------------------------------------------- /commands/local/delete/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import ora from 'ora'; 3 | import path from 'path'; 4 | import chalk from 'chalk'; 5 | import { get as get_build_path } from '../helpers/build_path/index.js'; 6 | 7 | import { LESS_LOCAL_FLAG } from '../constants/index.js'; 8 | 9 | const delete_app = async (project_name) => { 10 | const built_path = path.join( 11 | get_build_path(), 12 | project_name 13 | ); 14 | 15 | const spinner = ora(chalk.gray(`${LESS_LOCAL_FLAG} Deleting built project...`)); 16 | spinner.start(); 17 | 18 | const less_local_flag = chalk.yellowBright(LESS_LOCAL_FLAG); 19 | if (!fs.existsSync(built_path)) { 20 | spinner.stop(); 21 | console.log( 22 | less_local_flag, 23 | chalk.redBright(`The built with name "${project_name}" does not exist.`) 24 | ); 25 | process.exitCode = 1; 26 | return; 27 | } 28 | 29 | 30 | fs.rmSync(built_path, { recursive: true, force: true }); 31 | 32 | spinner.stop(); 33 | console.log( 34 | less_local_flag, 35 | chalk.greenBright(`The built with name "${project_name}" has been deleted. ✅`) 36 | ); 37 | }; 38 | 39 | export default delete_app; -------------------------------------------------------------------------------- /commands/local/helpers/add_to_package_json/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | /** 5 | * Store new information on the package json located to the build path 6 | * @param {Object} config - All the project build config data 7 | * @param {Object} data - The information to store on the package_json 8 | */ 9 | const add_to_package_json = (config, data) => { 10 | let package_json_path = path.join( 11 | config.project_build_path, 12 | config.javascript_dependencies_file_name 13 | ); 14 | 15 | let package_json_readed_content = { 16 | name: config.project_name, 17 | version: '1.0.0', 18 | main: 'app.js', 19 | license: 'MIT', 20 | dependencies: { }, 21 | devDependencies: { } 22 | }; 23 | 24 | if (!fs.existsSync(package_json_path)) { 25 | package_json_path = path.join( 26 | config.project_location, 27 | config.javascript_dependencies_file_name 28 | ); 29 | 30 | if (fs.existsSync(package_json_path)) { 31 | const project_package_json_content = JSON.parse( 32 | fs.readFileSync(package_json_path, 'utf-8') 33 | ); 34 | 35 | project_package_json_content.devDependencies = {}; 36 | package_json_readed_content = project_package_json_content; 37 | } 38 | } else { 39 | package_json_readed_content = JSON.parse( 40 | fs.readFileSync(package_json_path, 'utf-8') 41 | ); 42 | } 43 | 44 | Object.keys(data).forEach(element => { 45 | Object.keys(data[element]).forEach(item => { 46 | if (!package_json_readed_content[element]) { 47 | package_json_readed_content[element] = {}; 48 | } 49 | package_json_readed_content[element][item] = data[element][item]; 50 | }); 51 | }); 52 | 53 | const express_package_json_path = path.join( 54 | config.project_build_path, 55 | config.javascript_dependencies_file_name 56 | ); 57 | 58 | if (!fs.existsSync(config.project_build_path)) { 59 | fs.mkdirSync(config.project_build_path); 60 | } 61 | 62 | fs.writeFileSync( 63 | express_package_json_path, 64 | JSON.stringify(package_json_readed_content, null, 2) 65 | ); 66 | }; 67 | 68 | export default add_to_package_json; -------------------------------------------------------------------------------- /commands/local/helpers/build_apis/build.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import map_dirs_recursive from '../map_dirs_recursive/index.js'; 4 | import add_to_package_json from '../add_to_package_json/index.js'; 5 | 6 | const api_code = `const cors = require('cors'); 7 | const routes = require('./routes'); 8 | const express = require('express'); 9 | 10 | const app = express(); 11 | 12 | const port = #port#; 13 | 14 | module.exports = () => { 15 | app.all('/', (req, res) => { 16 | res 17 | .status(403) 18 | .json({ error: 'Missing authentication token' }); 19 | }); 20 | 21 | app.use(cors()); 22 | app.use((req, res, next) => { 23 | // Store the headers in a custom object 24 | req.headers = {}; 25 | req.rawHeaders.forEach((value, index, array) => { 26 | if (index % 2 === 0) { 27 | req.headers[array[index]] = array[index + 1]; 28 | } 29 | }); 30 | next(); 31 | }); 32 | 33 | app.use('/', async (req, res, next) => { 34 | const body = [] 35 | req.body = await new Promise((resolve) => req.on('data', (chunk) => { 36 | body.push(chunk); 37 | }).on('end', () => { 38 | resolve(Buffer.concat(body).toString()); 39 | })).then(); 40 | 41 | next(); 42 | }, routes); 43 | 44 | app.listen(port); 45 | }; 46 | `; 47 | 48 | const apis_code = `#apis-import# 49 | 50 | module.exports = async () => { 51 | #apis-uses# 52 | }; 53 | `; 54 | 55 | const method_handler_snipped_code = `!#method-handler#.middlewares 56 | ? [(req, res, next) => next()] 57 | : #method-handler#.middlewares.map(middleware => route_handler(middleware)), 58 | route_handler(#method-handler#.process)`; 59 | 60 | const python_snipped_code = `def useMiddleware(middlewares, controller): 61 | def handler(req, res): 62 | middleware = middlewares[0] if middlewares else None 63 | response = None 64 | controller_to_execute = None 65 | 66 | for i in range(len(middlewares)): 67 | next_middleware = None 68 | 69 | def set_next(): 70 | nonlocal next_middleware 71 | if i + 1 < len(middlewares): 72 | nonlocal next_middleware 73 | next_middleware = middlewares[i + 1] 74 | else: 75 | nonlocal controller_to_execute 76 | controller_to_execute = controller 77 | 78 | response = middleware(req, res, set_next) 79 | 80 | if not next_middleware and not controller_to_execute: 81 | return response 82 | 83 | middleware = next_middleware 84 | 85 | response = controller_to_execute(req, res) 86 | return response 87 | 88 | return handler 89 | 90 | class InvalidMiddlewareError(Exception): 91 | def __init__(self, middleware_position): 92 | super().__init__(f"The middleware on index {{middleware_position}} is not a function.") 93 | `; 94 | 95 | const python_function_call = ` 96 | req = json.loads(data) 97 | res = {} 98 | 99 | route_handler = None 100 | process = #python-import#.process 101 | if hasattr(#python-import#, 'middlewares') and isinstance(#python-import#.middlewares, list): 102 | middlewares = #python-import#.middlewares 103 | 104 | for middleware in middlewares: 105 | if not callable(middleware): 106 | raise InvalidMiddlewareError(middlewares.index(middleware)) 107 | 108 | route_handler = useMiddleware(middlewares, process) 109 | else: 110 | route_handler = process 111 | 112 | response = route_handler(req, res) 113 | 114 | return response 115 | `; 116 | 117 | const javascript_calling_python_code = `const python_handler = require('#python-handler-dependency#'); 118 | 119 | exports.process = async (req, res) => { 120 | const response = await python_handler( 121 | JSON.stringify({ headers: req.headers, body: req.body }), 122 | '#handler-dir-path#' 123 | ); 124 | 125 | return JSON.parse(response); 126 | }; 127 | `; 128 | 129 | const route_handler_code = `const route_handler = (handler) => async (req, res, next) => { 130 | try { 131 | Object 132 | .keys(req.query || {}) 133 | .forEach( 134 | query => req.options.query[query] = 135 | req.originalUrl.split('?').pop() 136 | .split('&').find(item => item.startsWith(\`\${query}=\`)) 137 | .replace(\`\${query}=\`, '') 138 | ); 139 | 140 | req.params = { ...req.options.params }; 141 | req.query = { ...req.options.query }; 142 | 143 | const _res = { headers: {} }; 144 | const response = await handler(req, _res, next); 145 | 146 | if (!response && Object.keys(_res.headers).length) { 147 | res.set(_res.headers); 148 | } 149 | 150 | if (response) { 151 | if (typeof response.body === 'object') { 152 | console.log("The body response cannot be an object"); 153 | return res.status(502).send(); 154 | } 155 | 156 | if (response.headers) { 157 | res.set(response.headers); 158 | } 159 | 160 | return res 161 | .status(response.statusCode || 200) 162 | .send(typeof response.body !== 'number' 163 | ? response.body 164 | : JSON.stringify(response.body) 165 | ); 166 | } 167 | } catch(error) { 168 | console.error(error); 169 | 170 | return res.status(502).send('Internal Server Error'); 171 | } 172 | }` 173 | 174 | const api_routes_code = `const { Router } = require('express'); 175 | 176 | const less_routes = Router(); 177 | 178 | #route-handler# 179 | #routes# 180 | 181 | module.exports = less_routes; 182 | `; 183 | 184 | const route_use_handler_code = `less_routes.use( 185 | '/#route-url#', 186 | (req, res, next) => { 187 | const current_route = '#route-url#'; 188 | 189 | if (req.options) { 190 | req.options.path = (req.options.path || '/') + \`/\${current_route}\`; 191 | req.options.params = { ...req.options.params, ...req.params }; 192 | req.options.query = { ...req.options.query, ...req.query }; 193 | 194 | } else { 195 | req.options = { 196 | path: \`/\${current_route}\`, 197 | params: req.params, 198 | query: req.query || {} 199 | }; 200 | } 201 | 202 | if (/^:.*$/.test(current_route)) { 203 | req.options.params[current_route.split(':')[1]] = 204 | req.originalUrl.split('/')[req.options.path.split('/').indexOf(current_route)]; 205 | } 206 | 207 | next(); 208 | }, 209 | #route-handler# 210 | );` 211 | 212 | const construct_api_routes = (config, data) => { 213 | const { api_routes_mapped, stack_path, less_built_api_path, less_api_path } = data; 214 | const methods_import_joined = []; 215 | const routes_handlers_import_joined = []; 216 | const methods_joined = []; 217 | const routes_handlers_joined = []; 218 | 219 | Object.keys(api_routes_mapped).forEach(api_route_mapped => { 220 | const [route] = api_route_mapped.split('.'); 221 | 222 | if (/^(get|post|put|patch|delete)\.(js|ts|py)$/.test(api_route_mapped)) { 223 | const stack_path_splited = stack_path.split('/'); 224 | stack_path_splited.shift(); 225 | 226 | const project_route_method_code = fs.readFileSync(path.resolve( 227 | less_api_path, 228 | stack_path_splited.join('/'), 229 | api_route_mapped 230 | ), 'utf-8'); 231 | 232 | const method_handler = `${route}_handler`; 233 | 234 | if (api_route_mapped.endsWith('.js') || api_route_mapped.endsWith('.ts')) { 235 | const file_extension = api_route_mapped.split('.').pop(); 236 | const build_route_path = path.resolve( 237 | less_built_api_path, 238 | stack_path 239 | ); 240 | 241 | fs.mkdirSync(build_route_path, { recursive: true }); 242 | fs.writeFileSync( 243 | path.resolve( 244 | build_route_path, 245 | `${method_handler}.${file_extension}` 246 | ), 247 | project_route_method_code 248 | ); 249 | } else { 250 | const handler_dir_path = path.resolve( 251 | less_built_api_path, 252 | stack_path, 253 | method_handler 254 | ); 255 | const python_method_handler =`${route.split('.')[0]}_method`; 256 | 257 | fs.mkdirSync(handler_dir_path, { recursive: true }); 258 | 259 | fs.writeFileSync( 260 | path.resolve( 261 | handler_dir_path, 262 | `${python_method_handler}.py` 263 | ), 264 | project_route_method_code 265 | ); 266 | 267 | fs.writeFileSync( 268 | path.resolve( 269 | handler_dir_path, 270 | `python_handler.py` 271 | ), 272 | config.python_handler 273 | .replace('#python-import#', `import ${python_method_handler}`) 274 | .replace('#python-snipped-code#', python_snipped_code) 275 | .replace('#python-function-call#', python_function_call.replaceAll('#python-import#', python_method_handler)) 276 | ); 277 | 278 | fs.writeFileSync( 279 | path.resolve( 280 | handler_dir_path, 281 | `index.js` 282 | ), 283 | javascript_calling_python_code 284 | .replace( 285 | '#handler-dir-path#', 286 | handler_dir_path 287 | ) 288 | .replace( 289 | '#python-handler-dependency#', 290 | config.python_handler_dependency 291 | ) 292 | ); 293 | } 294 | 295 | methods_import_joined.push(`const ${method_handler} = require('./${method_handler}');`); 296 | methods_joined.push( 297 | `less_routes.${route}(\n '/',\n ${ 298 | method_handler_snipped_code 299 | .replaceAll('#method-handler#', method_handler) 300 | });` 301 | ); 302 | 303 | return; 304 | } 305 | 306 | if (/\..*$/.test(api_route_mapped)) { 307 | const method_handler_code = fs.readFileSync(path.resolve( 308 | less_api_path, 309 | stack_path.replace(`${config.api_routes}/`, ''), 310 | api_route_mapped 311 | ), 'utf-8'); 312 | 313 | fs.writeFileSync( 314 | path.resolve( 315 | less_built_api_path, 316 | stack_path, 317 | api_route_mapped 318 | ), 319 | method_handler_code 320 | ); 321 | return; 322 | } 323 | 324 | const new_path = path.resolve( 325 | less_built_api_path, 326 | stack_path, 327 | api_route_mapped 328 | ) 329 | 330 | fs.mkdirSync(new_path, { recursive: true }); 331 | 332 | const route_handler_name = `less_route_${api_route_mapped.replace('{', '').replace('}', '')}`; 333 | 334 | routes_handlers_import_joined.push(`const ${route_handler_name} = require('./${api_route_mapped}');`); 335 | routes_handlers_joined.push(route_use_handler_code 336 | .replace('#route-handler#', route_handler_name) 337 | .replaceAll('#route-url#', api_route_mapped.replace('{', ':').replace('}', '')) 338 | ); 339 | 340 | const new_data = { 341 | api_routes_mapped: api_routes_mapped[api_route_mapped], 342 | stack_path: stack_path + `/${api_route_mapped}`, 343 | less_built_api_path, 344 | less_api_path 345 | }; 346 | 347 | construct_api_routes(config, new_data); 348 | }); 349 | 350 | const all_import_joined = []; 351 | if (methods_import_joined.length) { 352 | all_import_joined.push(methods_import_joined.join('\n')) 353 | } 354 | if (routes_handlers_import_joined.length) { 355 | all_import_joined.push(routes_handlers_import_joined.join('\n')) 356 | } 357 | 358 | const all_uses_joined = []; 359 | if (methods_joined.length) { 360 | all_uses_joined.push(methods_joined.join('\n')) 361 | } 362 | if (routes_handlers_joined.length) { 363 | all_uses_joined.push(routes_handlers_joined.join('\n')) 364 | } 365 | 366 | fs.writeFileSync( 367 | path.resolve( 368 | less_built_api_path, 369 | stack_path, 370 | 'index.js' 371 | ), 372 | api_routes_code 373 | .replace('#route-handler#', all_uses_joined.find(item => /route_handler(.*)/.test(item)) ? `${route_handler_code}\n` : '') 374 | .replace('#routes#', [ 375 | all_import_joined.join('\n\n'), 376 | all_uses_joined.join('\n\n') 377 | ].join('\n\n\n')) 378 | ); 379 | 380 | add_to_package_json(config, { 381 | dependencies: { 382 | express: '^4.18.3', 383 | ws: "^8.16.0", 384 | } 385 | }); 386 | } 387 | 388 | export default (config) => { 389 | const apis_path = path.resolve( 390 | config.project_less_resources_location, 391 | config.less_resources.apis, 392 | ); 393 | 394 | if (!fs.existsSync(apis_path)) { 395 | return; 396 | } 397 | 398 | config.app_imports += 'const apis = require(\'./apis\');\n'; 399 | config.app_callers += 'apis();\n'; 400 | 401 | const apis = fs.readdirSync(apis_path); 402 | 403 | 404 | let apis_uses = ''; 405 | let apis_imports = ''; 406 | apis.forEach((api, index) => { 407 | config.apis[api] = { 408 | port: config.rest_api_port + index 409 | }; 410 | 411 | const less_built_api_path = path.resolve( 412 | config.project_build_path, 413 | config.less_resources.apis, 414 | api 415 | ); 416 | 417 | const less_api_path = path.resolve( 418 | config.project_less_resources_location, 419 | config.less_resources.apis, 420 | api 421 | ); 422 | 423 | fs.mkdirSync(less_built_api_path, { recursive: true }); 424 | 425 | const api_resources = map_dirs_recursive(less_api_path); 426 | 427 | const data = { 428 | api_routes_mapped: api_resources, 429 | stack_path: config.api_routes, 430 | less_built_api_path, 431 | less_api_path 432 | }; 433 | 434 | construct_api_routes(config, data); 435 | 436 | apis_imports += `\n`; 437 | apis_uses += `\n\n`; 438 | 439 | fs.writeFileSync( 440 | path.resolve( 441 | config.project_build_path, 442 | config.less_resources.apis, 443 | api, 444 | 'index.js' 445 | ), 446 | api_code 447 | .replace( 448 | '#port#', 449 | config.rest_api_port + index 450 | ) 451 | .replace( 452 | '#api#', 453 | api 454 | ) 455 | ) 456 | }); 457 | 458 | fs.writeFileSync( 459 | path.resolve( 460 | config.project_build_path, 461 | config.less_resources.apis, 462 | 'index.js' 463 | ), 464 | apis_code 465 | .replace( 466 | '#apis-import#', 467 | apis.map(api => `const ${api} = require('./${api}');`).join('\n') 468 | ) 469 | .replace( 470 | '#apis-uses#', 471 | apis.map(api => `${api}();`).join('\n ') 472 | ) 473 | ); 474 | } 475 | -------------------------------------------------------------------------------- /commands/local/helpers/build_apis/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import build from './build.js'; 4 | 5 | const topics_api_route = `const { topics } = require('@chuva.io/less'); 6 | 7 | exports.process = async (req, res) => { 8 | const { topic_id } = req.params; 9 | 10 | if (!topics[topic_id]) { 11 | res.statusCode = 404; 12 | res.body = JSON.stringify({ error: 'Topic not found' }); 13 | 14 | return res; 15 | } 16 | 17 | await topics[topic_id].publish(JSON.parse(req.body)); 18 | 19 | res.statusCode = 202; 20 | return res; 21 | }; 22 | `; 23 | 24 | export default (config) => { 25 | const project_topics_path = path.resolve( 26 | config.project_less_resources_location, 27 | config.less_resources.topics 28 | ); 29 | 30 | const created_topic_api_path = path.resolve( 31 | config.project_less_resources_location, 32 | config.less_resources.apis, 33 | 'topic', 34 | 'topics', 35 | '{topic_id}' 36 | ); 37 | 38 | const topics_resources_exists = fs.existsSync(project_topics_path) 39 | && Boolean(fs.readdirSync(project_topics_path).length); 40 | 41 | if (topics_resources_exists) { 42 | fs.mkdirSync(created_topic_api_path, { recursive: true }); 43 | fs.writeFileSync( 44 | path.resolve( 45 | created_topic_api_path, 46 | 'post.js' 47 | ), 48 | topics_api_route 49 | ); 50 | } 51 | 52 | build(config); 53 | 54 | if (fs.existsSync(created_topic_api_path)) { 55 | const path_to_remove = created_topic_api_path.split('/'); 56 | path_to_remove.pop(); 57 | path_to_remove.pop(); 58 | fs.rmSync(path_to_remove.join('/'), { recursive: true, force: true }); 59 | } 60 | } -------------------------------------------------------------------------------- /commands/local/helpers/build_cloud_functions/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | const callers_code = ` 5 | const path = require('path'); 6 | const python_handler = require('#python-handler-dependency#'); 7 | 8 | const handler = async (data, function_path) => { 9 | let response; 10 | if (!function_path.endsWith('.js')) { 11 | response = await python_handler(JSON.stringify(data), function_path); 12 | } else { 13 | response = await require(function_path).process(data); 14 | } 15 | 16 | return JSON.parse(response); 17 | }; 18 | 19 | module.exports = { 20 | #exports# 21 | } 22 | `; 23 | 24 | const functions_callers_exports = `#function#: async (data) => handler(data, path.resolve(__dirname, '#function_path#'))`; 25 | 26 | const build = (config) => { 27 | const functions_handlers_name = 'handlers'; 28 | const project_functions_path = path.resolve( 29 | config.project_less_resources_location, 30 | config.less_resources.functions 31 | ); 32 | 33 | if (!fs.existsSync(project_functions_path)) { 34 | return; 35 | } 36 | 37 | const functions = fs.readdirSync(project_functions_path); 38 | 39 | const built_functions_path = path.resolve( 40 | config.chuva_dependency_path, 41 | config.less_resources.functions 42 | ); 43 | const handlers_path = path.resolve( 44 | built_functions_path, 45 | functions_handlers_name 46 | ); 47 | 48 | fs.mkdirSync(handlers_path, { recursive: true }); 49 | const module_exports = functions 50 | .map( 51 | element => { 52 | const project_function_path = path.resolve( 53 | project_functions_path, 54 | element 55 | ); 56 | 57 | let function_handler_path = `${functions_handlers_name}/${element}`; 58 | 59 | if (fs.existsSync(project_function_path + '/__init__.py')) { 60 | const handler_path = path.join(handlers_path + `/${element}`); 61 | 62 | fs.mkdirSync(handler_path); 63 | fs.cpSync( 64 | project_functions_path + `/${element}`, 65 | handler_path, 66 | { recursive: true, filter: (src, dest) => !src.endsWith('__init__.py') } 67 | ); 68 | 69 | const handler_function_name = `${element}_handler`; 70 | fs.writeFileSync( 71 | path.join(handler_path, `${handler_function_name}.py`), 72 | fs.readFileSync( 73 | project_functions_path + `/${element}/__init__.py`, 74 | 'utf-8' 75 | ) 76 | ); 77 | 78 | fs.writeFileSync( 79 | path.join(handler_path, 'python_handler.py'), 80 | config.python_handler 81 | .replace( 82 | '#python-import#', 83 | `import ${handler_function_name}` 84 | ) 85 | .replace('#python-snipped-code#', '') 86 | .replace('#python-function-call#', `return ${handler_function_name}.process(json.loads(data))`) 87 | ); 88 | 89 | } else { 90 | fs.cpSync( 91 | project_functions_path + `/${element}`, 92 | handlers_path + `/${element}`, 93 | { recursive: true } 94 | ); 95 | 96 | function_handler_path += '/index.js'; 97 | } 98 | 99 | return functions_callers_exports 100 | .replace('#function#', element) 101 | .replace('#function_path#', function_handler_path) 102 | } 103 | ).join(',\n '); 104 | 105 | 106 | 107 | fs.writeFileSync( 108 | built_functions_path + '/index.js', 109 | callers_code 110 | .replace('#python-handler-dependency#', config.python_handler_dependency) 111 | .replace('#exports#', module_exports) 112 | ); 113 | }; 114 | 115 | export default build; 116 | -------------------------------------------------------------------------------- /commands/local/helpers/build_crons/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import map_dirs_recursive from '../map_dirs_recursive/index.js'; 4 | 5 | const crons_handlers_code = `#crons-imports# 6 | 7 | module.exports = { 8 | #crons-exports# 9 | };`; 10 | 11 | const cron_config_code = `const job_#cron-handler# = new CronJob( 12 | process.env.CRON_#cron-uppercase#.replace('?', '*'), 13 | async () => { 14 | try{ 15 | await handlers.#cron-handler#.process(); 16 | console.log(\`#less-local-info-tag#Cron '#cron-handler#' was successfully processed.#less-local-info-tag#\`); 17 | } catch(error) { 18 | console.log(\`#less-local-error-tag#Failed to process cron '#cron-handler#'. ERROR: <#\${error}#>#less-local-error-tag#\`); 19 | } 20 | }, 21 | null, 22 | false, 23 | time_zone 24 | );` 25 | 26 | const crons_code = `const { CronJob } = require('cron'); 27 | const handlers = require('./handlers'); 28 | 29 | const time_zone = Intl.DateTimeFormat().resolvedOptions().timeZone; 30 | 31 | #crons-configs# 32 | 33 | module.exports = async () => { 34 | #crons-starts# 35 | };` 36 | 37 | const build = (config) => { 38 | const project_crons_path = path.join( 39 | config.project_less_resources_location, 40 | config.less_resources.crons, 41 | ); 42 | 43 | if (!fs.existsSync(project_crons_path)) { 44 | return; 45 | } 46 | 47 | const crons = Object.keys(map_dirs_recursive(project_crons_path)); 48 | 49 | const less_built_crons_path = path.join( 50 | config.project_build_path, 51 | config.less_resources.crons 52 | ); 53 | 54 | const crons_handlers_path = path.join( 55 | less_built_crons_path, 56 | 'handlers' 57 | ); 58 | 59 | fs.mkdirSync(crons_handlers_path, { recursive: true }); 60 | fs.cpSync( 61 | project_crons_path, 62 | crons_handlers_path, 63 | { recursive: true } 64 | ); 65 | 66 | fs.writeFileSync( 67 | crons_handlers_path + '/index.js', 68 | crons_handlers_code 69 | .replace('#crons-exports#', crons.join(',\n')) 70 | .replace( 71 | '#crons-imports#', 72 | crons.map(cron => `const ${cron} = require('./${cron}');`).join('\n') 73 | ) 74 | ); 75 | 76 | fs.writeFileSync( 77 | less_built_crons_path + '/index.js', 78 | crons_code 79 | .replace( 80 | '#crons-configs#', 81 | crons.map(cron => cron_config_code 82 | .replaceAll('#cron-handler#', cron) 83 | .replace('#cron-uppercase#', cron.toUpperCase()) 84 | .replaceAll( 85 | '#less-local-error-tag#', 86 | config.less_local_error_flag 87 | ) 88 | .replaceAll( 89 | '#less-local-info-tag#', 90 | config.less_local_info_flag 91 | ) 92 | ).join('\n\n') 93 | ) 94 | .replace( 95 | '#crons-starts#', 96 | crons.map(cron => `job_${cron}.start();`).join('\n ') 97 | ) 98 | ); 99 | 100 | config.app_imports += 'const crons = require(\'./crons\');\n'; 101 | config.app_callers += 'crons();\n' 102 | } 103 | 104 | export default build; -------------------------------------------------------------------------------- /commands/local/helpers/build_less_dependencies/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | import check_resources from '../check_resources/index.js'; 5 | import add_to_package_json from '../add_to_package_json/index.js'; 6 | 7 | const kvs_handler_code = `const { #kvs-table-module# } = require('lessSqliteDB'); 8 | const topics = #topics-import#; 9 | 10 | const client = new #kvs-table-module#(); 11 | 12 | const events = { 13 | created: 'created', 14 | updated: 'updated', 15 | deleted: 'deleted' 16 | }; 17 | 18 | const publish_kvs_event = async (new_value, topic, event) => { 19 | const data = { 20 | key: new_value.id 21 | }; 22 | 23 | if (event === events.created || event === events.updated) { 24 | data.new_value = JSON.parse(new_value.value); 25 | } 26 | 27 | if (event === events.deleted || event === events.updated) { 28 | const item = await client.getOne({ id: new_value.id }); 29 | data.old_value = JSON.parse(item.value); 30 | } 31 | 32 | topic.publish(data); 33 | }; 34 | 35 | const kvs_get = async (key) => { 36 | if (!key) { 37 | throw new Error('Error: The param "key" must be provided'); 38 | } 39 | 40 | if (typeof key !== 'string') { 41 | throw new Error('Error: The param "key" must be of type string'); 42 | } 43 | 44 | const item = await client.getOne({ id: key }); 45 | 46 | return item ? JSON.parse(item.value) : null; 47 | }; 48 | 49 | const kvs_set = async (key, value, ttl) => { 50 | let date; 51 | if (!key) { 52 | throw new Error('Error: The param "key" must be provided'); 53 | } 54 | 55 | if (typeof key !== 'string') { 56 | throw new Error('Error: The param "key" must be of type string'); 57 | } 58 | 59 | if (!value) { 60 | throw new Error('Error: The param "value" must be provided'); 61 | } 62 | 63 | if (ttl) { 64 | if (typeof ttl !== 'number') { 65 | throw new Error('Error: The param "ttl" must be of type number'); 66 | } 67 | 68 | date = new Date(Date.now() + (ttl * 1000)).toISOString(); 69 | } 70 | 71 | const params = { id: key, value: JSON.stringify(value) }; 72 | if (date) { 73 | params.ttl = date; 74 | } 75 | 76 | const item = await client.getOne({ id: key }); 77 | 78 | if (item) { 79 | if (topics.kvs_updated) { 80 | await publish_kvs_event(params, topics.kvs_updated, events.updated); 81 | } 82 | 83 | delete params.id; 84 | await client.update({ 85 | columns: params, 86 | where: { id: key } 87 | }); 88 | } else { 89 | await client.create(params); 90 | 91 | if (topics.kvs_created) { 92 | await publish_kvs_event(params, topics.kvs_created, events.created); 93 | } 94 | } 95 | }; 96 | 97 | const kvs_delete = async (key) => { 98 | if (!key) { 99 | throw new Error('Error: The param "key" must be provided'); 100 | } 101 | 102 | if (typeof key !== 'string') { 103 | throw new Error('Error: The param "key" must be of type string'); 104 | } 105 | 106 | const params = { id: key }; 107 | 108 | if (topics.kvs_deleted) { 109 | await publish_kvs_event(params, topics.kvs_deleted, events.deleted); 110 | } 111 | 112 | await client.delete(params); 113 | }; 114 | 115 | module.exports = { 116 | get: kvs_get, 117 | set: kvs_set, 118 | delete: kvs_delete 119 | }; 120 | `; 121 | 122 | const chuva_depenencies_code = `const kvs = require(\'./kvs\'); 123 | #imports# 124 | module.exports = { 125 | kvs, 126 | #exports# 127 | };` 128 | 129 | const python_handler_code = `const { exec } = require('child_process'); 130 | 131 | const execute_python_handler = async (data, current_dir_path) => { 132 | const response = await new Promise( 133 | (resolve, reject) => ( 134 | exec( 135 | \`python3 \${current_dir_path}/python_handler.py --data \${JSON.stringify(data)}\`, 136 | (error, stdout, stderr) => { 137 | if (error) { 138 | reject(error); 139 | return; 140 | } 141 | 142 | const response = stdout 143 | .match( 144 | /#LESS-EXPRESS::RETURNING-RESPONSE::<\{.*\}>::RETURNING-RESPONSE::LESS-EXPRESS#/ 145 | )[0] 146 | .replace('#LESS-EXPRESS::RETURNING-RESPONSE::<{', '') 147 | .replace('}>::RETURNING-RESPONSE::LESS-EXPRESS#', ''); 148 | 149 | resolve(response); 150 | } 151 | ) 152 | ) 153 | ); 154 | 155 | return response; 156 | }; 157 | 158 | module.exports = execute_python_handler; 159 | `; 160 | 161 | const cron_delete_key_value_storage_items_code = `const { CronJob } = require('cron'); 162 | const { kvs } = require('@chuva.io/less'); 163 | const { #kvs-table-module# } = require('lessSqliteDB'); 164 | 165 | const time_zone = Intl.DateTimeFormat().resolvedOptions().timeZone; 166 | 167 | const cron = new CronJob( 168 | '* * * * * *', 169 | async () => { 170 | const client = new #kvs-table-module#(); 171 | const items = await client.getAll({ ttl: { '<=': new Date().toISOString() } }); 172 | await Promise.all(items.map(item => kvs.delete(item.id))); 173 | client.close(); 174 | if (items.length) { 175 | console.log( 176 | \`#less-local-info-tag#The items (\${items.map(item => item.id).join(', ')}) were deleted from kvs after reaching their ttl limit.#less-local-info-tag#\` 177 | ); 178 | } 179 | }, 180 | null, 181 | false, 182 | time_zone 183 | ); 184 | 185 | module.exports = () => { 186 | cron.start(); 187 | }; 188 | `; 189 | 190 | const build = (config) => { 191 | const resources_checked = check_resources( 192 | config.project_less_resources_location, 193 | config.less_resources 194 | ); 195 | 196 | fs.mkdirSync( 197 | config.chuva_dependency_path, 198 | { recursive: true } 199 | ); 200 | 201 | const topics_import = !fs.existsSync(path.resolve( 202 | config.project_less_resources_location, 203 | config.less_resources.topics 204 | )) 205 | ? '{}' 206 | : 'require(\'./topics\')'; 207 | 208 | fs.writeFileSync( 209 | config.chuva_dependency_path + '/kvs.js', 210 | kvs_handler_code 211 | .replace('#topics-import#', topics_import) 212 | .replaceAll('#kvs-table-module#', config.less_sqlite_tables.kvs.model) 213 | ); 214 | 215 | const resources_found = [ 216 | config.less_resources.topics, 217 | config.less_resources.sockets, 218 | config.less_resources.functions 219 | ].filter( 220 | resource => Object.keys(resources_checked).includes(resource) 221 | ); 222 | 223 | fs.writeFileSync( 224 | config.chuva_dependency_path + '/index.js', 225 | chuva_depenencies_code 226 | .replace( 227 | '#imports#', 228 | resources_found.map(resource => `const ${resource} = require('./${resource}');\n`).join('') 229 | ) 230 | .replace( 231 | '#exports#', 232 | resources_found.map(resource => resource).join(',\n ') 233 | ) 234 | ); 235 | 236 | const python_handler_path = path.resolve( 237 | config.project_build_path, 238 | config.python_handler_dependency 239 | ); 240 | 241 | fs.mkdirSync(python_handler_path, { recursive: true }); 242 | fs.writeFileSync( 243 | python_handler_path + '/index.js', 244 | python_handler_code 245 | ); 246 | 247 | const crons_path = path.resolve( 248 | config.project_build_path, 249 | 'cron_delete_key_value_storage_items' 250 | ); 251 | 252 | fs.mkdirSync(crons_path, { recursive: true }); 253 | fs.writeFileSync( 254 | crons_path + '/index.js', 255 | cron_delete_key_value_storage_items_code 256 | .replaceAll('#less-local-info-tag#', config.less_local_info_flag) 257 | .replaceAll('#kvs-table-module#', config.less_sqlite_tables.kvs.model) 258 | ); 259 | 260 | config.app_imports += 'const cron_delete_key_value_storage_items = require(\'./cron_delete_key_value_storage_items\');\n'; 261 | config.app_callers += 'cron_delete_key_value_storage_items();\n'; 262 | 263 | config.content_paths_to_delete.push(config.chuva_dependency_path); 264 | add_to_package_json(config, { 265 | devDependencies: { 266 | [config.chuva_dependency]: `file:./${config.chuva_dependency}`, 267 | [config.python_handler_dependency]: `file:./${config.python_handler_dependency}` 268 | } 269 | }); 270 | } 271 | 272 | export default build; -------------------------------------------------------------------------------- /commands/local/helpers/build_path/index.js: -------------------------------------------------------------------------------- 1 | import os from 'os'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | 5 | export const get = () => { 6 | const builds_path = path.join( 7 | os.homedir(), 8 | '.less-cli', 9 | 'builds' 10 | ); 11 | 12 | if (!fs.existsSync(builds_path)) { 13 | fs.mkdirSync(builds_path, { recursive: true }); 14 | } 15 | 16 | return builds_path; 17 | }; -------------------------------------------------------------------------------- /commands/local/helpers/build_shared_code/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import map_dirs_recursive from '../map_dirs_recursive/index.js'; 4 | import add_to_package_json from '../add_to_package_json/index.js'; 5 | 6 | const build = (config) => { 7 | const shared_path = path.resolve( 8 | config.project_less_resources_location, 9 | config.less_resources.shared 10 | ); 11 | 12 | if (!fs.existsSync(shared_path)) { 13 | return; 14 | } 15 | 16 | const shared_modules = map_dirs_recursive(shared_path); 17 | 18 | if (!shared_modules || shared_modules.length) { 19 | return; 20 | } 21 | 22 | const devDependencies = {}; 23 | Object.keys(shared_modules).forEach(element => { 24 | if (shared_modules[element]['index.js'] === null) { 25 | config.shared.push(element); 26 | devDependencies[element] = `${path.join(shared_path, element)}`; 27 | } 28 | }); 29 | 30 | add_to_package_json(config, { devDependencies }); 31 | } 32 | 33 | export default build; -------------------------------------------------------------------------------- /commands/local/helpers/build_sockets/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import map_dirs_recursive from '../map_dirs_recursive/index.js'; 4 | import add_to_package_json from '../add_to_package_json/index.js'; 5 | import convert_snake_to_camel_case from '../convert_snake_to_camel_case/index.js'; 6 | 7 | const socket_client_class_code = `class #client-class# { 8 | constructor() { 9 | if (#client-class#.exists) { 10 | console.log('#client-class# already exists'); 11 | return #client-class#.instance; 12 | } 13 | 14 | this.clients = new Map(); 15 | #client-class#.exists = true; 16 | #client-class#.instance = this.clients; 17 | return this.clients; 18 | } 19 | };` 20 | 21 | const connect_handler_code = `const response = await require('./connect').process({ connection_id }); 22 | const verify_status_code_in_range = (range, status) => Array(100) 23 | .fill(range) 24 | .map((n, i) => n + i) 25 | .includes(status); 26 | 27 | if ( 28 | verify_status_code_in_range(400, (response && response.statusCode) || null) 29 | || verify_status_code_in_range(500, (response && response.statusCode) || null) 30 | ) { 31 | socket.close(1008, \`HTTP/1.1 \${response.statusCode} \\r\\n\\r\\n\`); 32 | return; 33 | }`; 34 | 35 | const message_handler_code = `const body_message = JSON.parse(data.toString('utf-8')); 36 | const channels = require('./channels'); 37 | if (!channels[body_message.channel]) { 38 | socket.message(\`HTTP/1.1 404 Channel not found \\r\\n\\r\\n\`); 39 | return; 40 | } 41 | await channels[body_message.channel].process({ connection_id, data: body_message.data });`; 42 | 43 | const socket_code = `const Websocket = require('ws'); 44 | const { randomUUID } = require('crypto'); 45 | const { #client-class# } = require('#client-classes-import#'); 46 | 47 | const port = #socket-port#; 48 | const ws = new Websocket.WebSocketServer({ port }); 49 | 50 | module.exports = () => { 51 | ws.on('connection', async (socket) => { 52 | const client = new #client-class#(); 53 | const connection_id = randomUUID(); 54 | socket.id = connection_id; 55 | 56 | #connect-handler-code# 57 | 58 | client.set(connection_id, { socket }); 59 | 60 | socket.on('close', async () => { 61 | #disconnect-handler-code# 62 | }); 63 | 64 | socket.on('message', async (data) => { 65 | #message-handler-code# 66 | }); 67 | }); 68 | }; 69 | `; 70 | 71 | const socket_publisher_code = `const { #client-class# } = require('#client-classes-import#'); 72 | 73 | const clients = new #client-class#(); 74 | 75 | module.exports = async (message, connections) => { 76 | const sockets = []; 77 | if (connections && message) { 78 | for (let i = 0; i < connections.length; i++) { 79 | if (clients.has(connections[i])) { 80 | sockets.push(clients.get(connections[i])); 81 | } 82 | } 83 | 84 | await Promise.all( 85 | sockets.map( 86 | async ({ socket }) => socket.send(JSON.stringify(message)) 87 | ) 88 | ); 89 | } 90 | };`; 91 | 92 | const construct_socket_clients_maping = (config, sockets) => { 93 | const clients_path = path.join( 94 | config.project_build_path, 95 | config.less_websocket_clients 96 | ); 97 | 98 | const clients = []; 99 | const module_exports = []; 100 | 101 | sockets.forEach(socket => { 102 | const client_class = `${convert_snake_to_camel_case(socket)}Client`; 103 | 104 | clients.push( 105 | socket_client_class_code.replaceAll('#client-class#', client_class) 106 | ); 107 | 108 | module_exports.push(`${client_class}`); 109 | }); 110 | 111 | fs.mkdirSync(clients_path, { recursive: true }); 112 | fs.writeFileSync( 113 | clients_path + '/index.js', 114 | `${clients.join('\n\n')} 115 | 116 | module.exports = { 117 | ${module_exports.join(',\n ')} 118 | };`); 119 | 120 | add_to_package_json(config,{ 121 | devDependencies: { 122 | [config.less_websocket_clients]: `./${config.less_websocket_clients}` 123 | } 124 | }); 125 | 126 | config.content_paths_to_delete.push(path.join( 127 | config.project_build_path, 128 | config.less_websocket_clients 129 | )); 130 | } 131 | 132 | const construct_sockets_handlers = (config, data) => { 133 | const { sockets_resources, stack_path, port } = data; 134 | 135 | const stack_path_splited = stack_path.split('/'); 136 | const less_socket_path = path.join( 137 | config.project_less_resources_location, 138 | stack_path 139 | ); 140 | const less_built_socket_path = 141 | path.join(config.project_build_path, stack_path); 142 | 143 | if (stack_path_splited.length > 1) { 144 | let less_built_channels_path; 145 | let less_built_channels_imports = ''; 146 | let less_built_channels_exports = ''; 147 | const client_class = `${convert_snake_to_camel_case(stack_path_splited[1])}Client`; 148 | 149 | let connect_handler = `console.log('Client with connection_d \${connection_id} has connected.')`; 150 | let disconnect_handler = `console.log('Client with connection_d \${connection_id} has disconnected.');` 151 | let message_handler = `socket.send(\`HTTP/1.1 404 Channel not found \\r\\n\\r\\n\`);` 152 | 153 | const connect = 'connect'; 154 | const disconnect = 'disconnect'; 155 | Object.keys(sockets_resources).forEach(handler => { 156 | const less_built_socket_handler_path = 157 | path.join(less_built_socket_path, handler); 158 | 159 | fs.mkdirSync(less_built_socket_handler_path, { recursive: true }); 160 | if (handler === connect) { 161 | fs.cpSync( 162 | path.join(less_socket_path, connect), 163 | less_built_socket_handler_path, 164 | { recursive: true } 165 | ); 166 | 167 | connect_handler = connect_handler_code; 168 | return; 169 | } 170 | if (handler === disconnect) { 171 | fs.cpSync( 172 | path.join(less_socket_path, disconnect), 173 | less_built_socket_handler_path, 174 | { recursive: true } 175 | ); 176 | 177 | disconnect_handler = `await require('./disconnect').process({ connection_id });` 178 | return; 179 | } 180 | 181 | less_built_channels_path = 182 | path.join(less_built_socket_path, 'channels'); 183 | 184 | const less_built_channel_path = 185 | path.join(less_built_channels_path, handler); 186 | 187 | fs.mkdirSync(less_built_channel_path, { recursive: true }); 188 | fs.cpSync( 189 | path.join(less_socket_path, handler), 190 | less_built_channel_path, 191 | { recursive: true } 192 | ); 193 | 194 | less_built_channels_imports += `const ${handler} = require('./${handler}');\n`; 195 | less_built_channels_exports += ` ${handler},\n`; 196 | 197 | message_handler = message_handler_code; 198 | }); 199 | 200 | if (less_built_channels_path) { 201 | fs.writeFileSync( 202 | less_built_channels_path + '/index.js', 203 | `${less_built_channels_imports} 204 | module.exports = { 205 | ${less_built_channels_exports}}`); 206 | } 207 | 208 | fs.writeFileSync( 209 | less_built_socket_path + '/index.js', 210 | socket_code 211 | .replaceAll('#client-class#', client_class) 212 | .replace('#client-classes-import#', config.less_websocket_clients) 213 | .replace('#socket-port#', port) 214 | .replace('#socket-name#', stack_path_splited.at(-1)) 215 | .replace('#connect-handler-code#', connect_handler) 216 | .replace('#disconnect-handler-code#', disconnect_handler) 217 | .replace('#message-handler-code#', message_handler) 218 | ); 219 | 220 | return; 221 | } 222 | 223 | Object.keys(sockets_resources).forEach((socket, index) => { 224 | const socket_port = port + index; 225 | config.sockets[socket] = { 226 | port: socket_port 227 | }; 228 | 229 | const new_data = { 230 | port: socket_port, 231 | stack_path: stack_path + `/${socket}`, 232 | sockets_resources: sockets_resources[socket] 233 | }; 234 | 235 | construct_sockets_handlers(config, new_data); 236 | }); 237 | } 238 | 239 | const construct_socket_publishers = (config, sockets) => { 240 | const socket_publishers_path = path.join( 241 | config.project_build_path, 242 | config.chuva_dependency, 243 | config.less_resources.sockets 244 | ); 245 | 246 | sockets.forEach(socket => { 247 | const client = `${convert_snake_to_camel_case(socket)}Client`; 248 | 249 | const socket_publisher_path = 250 | path.join(socket_publishers_path, socket); 251 | 252 | fs.mkdirSync(socket_publisher_path, { recursive: true }); 253 | fs.writeFileSync( 254 | socket_publisher_path + '/index.js', 255 | socket_publisher_code 256 | .replaceAll('#client-class#', client) 257 | .replace('#client-classes-import#', config.less_websocket_clients) 258 | ); 259 | }); 260 | 261 | fs.writeFileSync( 262 | socket_publishers_path + '/index.js', 263 | `${sockets.map(socket => `const ${socket} = require('./${socket}');`).join('\n')} 264 | 265 | module.exports = { 266 | ${sockets.map(socket => `${socket}: { 267 | publish: ${socket} 268 | }`).join(',\n ')} 269 | };`) 270 | } 271 | 272 | const build = (config) => { 273 | const less_built_sockets_path = path.join( 274 | config.project_build_path, 275 | config.less_resources.sockets 276 | ); 277 | 278 | const less_project_sockets_path = path.join( 279 | config.project_less_resources_location, 280 | config.less_resources.sockets 281 | ); 282 | 283 | if (!fs.existsSync(less_project_sockets_path)) { 284 | return; 285 | } 286 | 287 | config.app_imports += 'const sockets = require(\'./sockets\');\n'; 288 | config.app_callers += 'sockets();\n' 289 | 290 | const sockets_resources = 291 | map_dirs_recursive(less_project_sockets_path); 292 | const sockets = Object.keys(sockets_resources); 293 | 294 | construct_socket_clients_maping(config, sockets); 295 | 296 | const handlers_data = { 297 | sockets_resources, 298 | port: config.socket_port, 299 | stack_path: config.less_resources.sockets 300 | }; 301 | 302 | construct_sockets_handlers(config, handlers_data); 303 | 304 | fs.writeFileSync( 305 | less_built_sockets_path + '/index.js', 306 | `${sockets.map(socket => `const ${socket} = require('./${socket}');`).join('\n')} 307 | 308 | module.exports = async () => { 309 | ${sockets.map(socket => `${socket}();`).join('\n ')} 310 | }; 311 | ` 312 | ); 313 | 314 | construct_socket_publishers(config, sockets); 315 | 316 | add_to_package_json(config, { 317 | dependencies: { 318 | ws: "^8.16.0", 319 | } 320 | }); 321 | }; 322 | 323 | export default build; 324 | -------------------------------------------------------------------------------- /commands/local/helpers/build_sqlite_database_dependency/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import slite3 from 'sqlite3'; 3 | import path, { dirname } from 'path'; 4 | import { fileURLToPath } from 'url'; 5 | 6 | import add_to_package_json from '../add_to_package_json/index.js'; 7 | import convert_snake_to_camel_case from '../convert_snake_to_camel_case/index.js'; 8 | 9 | const config_code = `const sqlite3 = require('sqlite3').verbose(); 10 | const path = require('path'); 11 | 12 | class LessSqliteDB { 13 | #db; 14 | constructor() { 15 | this.#db = new sqlite3.Database( 16 | '#slite-database-path#', 17 | sqlite3.OPEN_READWRITE, (err) => { 18 | if (err) { 19 | console.log('Connecting to sqlite error: ', err); 20 | throw err; 21 | } 22 | } 23 | ); 24 | } 25 | 26 | close() { 27 | this.#db.close() 28 | } 29 | 30 | #execute_query = async (query) => { 31 | return (new Promise((resolve, reject) => this.#db.all(query, (err, rows) => { 32 | if (err) { 33 | reject(err); 34 | } else { 35 | resolve(rows); 36 | } 37 | } 38 | ))).then(); 39 | } 40 | 41 | #map_value = (value) => { 42 | return typeof value === 'string' 43 | ? \`'\${value}'\` 44 | : value 45 | } 46 | 47 | async create(table, columns) { 48 | const columns_to_insert = Object.keys(columns); 49 | 50 | const values_to_insert = Object.values(columns).map(this.#map_value); 51 | 52 | const result = await this.#execute_query( 53 | \`INSERT INTO \${table} (\${columns_to_insert.join(', ')}) VALUES(\${values_to_insert.join(', ')});\` 54 | ); 55 | 56 | return result; 57 | } 58 | 59 | async update(table, { columns, where }) { 60 | const set_query = Object.keys(columns).map( 61 | column => \`\${column} = \${this.#map_value(columns[column])}\` 62 | ).join(', '); 63 | 64 | const where_query = Object.keys(where).map(column => { 65 | if (typeof where[column] == 'object') { 66 | const operator = Object.keys(where[column])[0]; 67 | return \`\${column} \${operator} \${ 68 | Array.isArray(where[column][operator]) 69 | ? \`(\${where[column][operator] 70 | .map(this.#map_value) 71 | .join(', ')})\` 72 | : this.#map_value(where[column][operator]) 73 | }\` 74 | } 75 | return \`\${column} = \${this.#map_value(where[column])}\`}) 76 | .join(' AND'); 77 | 78 | const result = await this.#execute_query( 79 | \`UPDATE \${table} SET \${set_query}\${where_query ? \` WHERE \${where_query}\` : ''};\` 80 | ); 81 | 82 | return result; 83 | } 84 | 85 | async getOne(table, where) { 86 | const option = Object 87 | .keys(where) 88 | .map(column => { 89 | if (typeof where[column] == 'object') { 90 | const operator = Object.keys(where[column])[0]; 91 | return \`\${column} \${operator} \${ 92 | Array.isArray(where[column][operator]) 93 | ? \`(\${where[column][operator] 94 | .map(this.#map_value) 95 | .join(', ')})\` 96 | : this.#map_value(where[column][operator]) 97 | }\` 98 | } 99 | return \`\${column} = \${this.#map_value(where[column])}\`}) 100 | .join(' AND'); 101 | 102 | const result = await this.#execute_query( 103 | \`SELECT * FROM \${table} WHERE \${option};\` 104 | ); 105 | 106 | return result[0]; 107 | } 108 | 109 | async getAll(table, where) { 110 | let query = \`SELECT * FROM \${table}\`; 111 | if (where) { 112 | query += \` WHERE \${Object.keys(where).map(column => { 113 | if (typeof where[column] == 'object') { 114 | const operator = Object.keys(where[column])[0]; 115 | return \`\${column} \${operator} \${ 116 | Array.isArray(where[column][operator]) 117 | ? \`(\${where[column][operator] 118 | .map(this.#map_value) 119 | .join(', ')})\` 120 | : this.#map_value(where[column][operator]) 121 | }\` 122 | } 123 | return \`\${column} = \${this.#map_value(where[column])}\`}) 124 | .join(' AND')}\`; 125 | } 126 | query += ';'; 127 | 128 | const result = await this.#execute_query(query); 129 | 130 | return result; 131 | } 132 | 133 | async delete(table, where) { 134 | const option = Object 135 | .keys(where) 136 | .map(column => { 137 | if (typeof where[column] == 'object') { 138 | const operator = Object.keys(where[column])[0]; 139 | return \`\${column} \${operator} \${ 140 | Array.isArray(where[column][operator]) 141 | ? \`(\${where[column][operator] 142 | .map(this.#map_value) 143 | .join(', ')})\` 144 | : this.#map_value(where[column][operator]) 145 | }\` 146 | } 147 | return \`\${column} = \${this.#map_value(where[column])}\`}) 148 | .join(' AND'); 149 | 150 | const result = await this.#execute_query(\`DELETE FROM \${table} WHERE \${option};\`); 151 | 152 | return result; 153 | } 154 | }; 155 | 156 | module.exports = LessSqliteDB; 157 | `; 158 | 159 | const dependency_code = `const config = require('./#db-folder#'); 160 | 161 | class TableModule extends config { 162 | constructor(table_name) { 163 | super(); 164 | this.table_name = table_name; 165 | } 166 | 167 | close() { 168 | return super.close(); 169 | } 170 | 171 | async create(payload) { 172 | return super.create(this.table_name, payload); 173 | } 174 | 175 | async update({ columns, where }) { 176 | return super.update(this.table_name, { columns, where }); 177 | } 178 | 179 | async getOne(where) { 180 | return super.getOne(this.table_name, where); 181 | } 182 | 183 | async getAll(where) { 184 | return super.getAll(this.table_name, where); 185 | } 186 | 187 | async delete(where) { 188 | return super.delete(this.table_name, where); 189 | } 190 | }; 191 | 192 | #resources-db# 193 | 194 | module.exports = { 195 | #resources-db-exports# 196 | }; 197 | `; 198 | 199 | const table_module_snipped_code = `class #table-module# extends TableModule { 200 | constructor() { 201 | super('#table#'); 202 | } 203 | }; 204 | `; 205 | 206 | const build = async (config) => { 207 | const __dirname = dirname(fileURLToPath(import.meta.url)); 208 | 209 | if (!fs.existsSync(path.resolve(config.project_build_path, config.less_sqlite_db))) { 210 | const newdb = new slite3.Database(path.resolve(config.project_build_path, config.less_sqlite_db)); 211 | await new Promise((res, rej) => newdb.exec( 212 | fs.readFileSync(__dirname + '/query.sql', 'utf8'), 213 | (error) => { 214 | if (error) { 215 | rej(error); 216 | } 217 | res(); 218 | } 219 | )); 220 | newdb.close(); 221 | } else { 222 | const db_dependency_path = path.join( 223 | config.project_build_path, 224 | 'node_modules', 225 | config.sqlite_database_dependency, 226 | 'index.js' 227 | ); 228 | 229 | if (fs.existsSync(db_dependency_path)) { 230 | const topics_db_model = 231 | (await import(db_dependency_path))[config.less_sqlite_tables.topics.model]; 232 | 233 | const client = new topics_db_model(); 234 | 235 | const items = await client.getAll(); 236 | 237 | if (items.length) { 238 | await client.update({ 239 | columns: { retrying: false }, 240 | where: { 241 | id: { 'in': items.map(item => item.id) } 242 | } 243 | }); 244 | } 245 | 246 | client.close(); 247 | } 248 | } 249 | 250 | const less_sqlite = path.resolve( 251 | config.project_build_path, 252 | config.sqlite_database_dependency 253 | ); 254 | const db_folder = 'config'; 255 | 256 | const db_path = path.resolve( 257 | less_sqlite, 258 | db_folder 259 | ); 260 | 261 | fs.mkdirSync(db_path, { recursive: true}); 262 | fs.writeFileSync( 263 | db_path + '/index.js', 264 | config_code.replace( 265 | '#slite-database-path#', 266 | path.resolve(config.project_build_path, config.less_sqlite_db) 267 | ) 268 | ); 269 | 270 | const resources_db = Object.values(config.less_sqlite_tables).map(item => { 271 | return { 272 | export: item.model, 273 | code: table_module_snipped_code 274 | .replaceAll('#table#', item.table) 275 | .replace('#table-module#', item.model) 276 | }; 277 | }); 278 | 279 | fs.writeFileSync( 280 | less_sqlite + '/index.js', 281 | dependency_code 282 | .replace('#db-folder#', db_folder) 283 | .replace('#resources-db#', resources_db.map(el => el.code).join('\n\n')) 284 | .replace('#resources-db-exports#', resources_db.map(el => el.export).join(',\n ')) 285 | ); 286 | 287 | add_to_package_json(config, { 288 | dependencies: { 289 | sqlite3: "^5.1.7", 290 | cors: "^2.8.5" 291 | }, 292 | devDependencies: { 293 | [config.sqlite_database_dependency]: `file:./${config.sqlite_database_dependency}` 294 | } 295 | }); 296 | }; 297 | 298 | export default build; -------------------------------------------------------------------------------- /commands/local/helpers/build_sqlite_database_dependency/query.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE topics_processors_queues ( 2 | id VARCHAR(100) NOT NULL, 3 | topic VARCHAR(500) NOT NULL, 4 | processor VARCHAR(500) NOT NULL, 5 | message JSON NOT NULL, 6 | retrying BOOLEAN NOT NULL, 7 | times_retried INTEGER NOT NULL, 8 | created_at TIMESTAMP NOT NULL, 9 | PRIMARY KEY(id) 10 | ); 11 | 12 | CREATE TABLE key_value_storage ( 13 | id VARCHAR(500) NOT NULL, 14 | value JSON NOT NULL, 15 | ttl DATE NULL, 16 | PRIMARY KEY(id) 17 | ); 18 | -------------------------------------------------------------------------------- /commands/local/helpers/build_topics/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | import map_dirs_recursive from '../map_dirs_recursive/index.js'; 5 | 6 | const publishers_code = `const fs = require('fs'); 7 | const path = require('path'); 8 | const { #topics-table-module# } = require('#database-dependency#'); 9 | const { randomUUID } = require('crypto'); 10 | 11 | const topic_handler = async (topic_name, topic_data) => { 12 | const topic_path = path.resolve( 13 | __dirname, 14 | 'handlers', 15 | topic_name 16 | ); 17 | 18 | const topic_processors_names = fs.readdirSync(topic_path); 19 | 20 | const processors = topic_processors_names 21 | .map( 22 | processor_name => require(\`./#topics-handlers#/\${topic_name}/\${processor_name}\`) 23 | ); 24 | 25 | Promise.all(processors.map( 26 | async (processor, index) => { 27 | try { 28 | await processor.process(topic_data); 29 | 30 | console.log( 31 | \`#less-local-info-tag#Processor "\${topic_processors_names[index]}" from topic "\${topic_name}" was successfully processed.#less-local-info-tag#\` 32 | ); 33 | } catch(error) { 34 | const message_id = randomUUID(); 35 | console.log( 36 | \`#less-local-error-tag#Failed to process topic '\${topic_name}' processor '\${topic_processors_names[index]}.The message has been stored for later processing. Error: <#\${error}#>#less-local-error-tag#'\` 37 | ); 38 | 39 | const topics = new #topics-table-module#(); 40 | 41 | await topics.create({ 42 | retrying: false, 43 | id: message_id, 44 | times_retried: 0, 45 | topic: topic_name, 46 | message: JSON.stringify(topic_data), 47 | created_at: new Date().toISOString(), 48 | processor: topic_processors_names[index], 49 | }); 50 | 51 | topics.close(); 52 | } 53 | } 54 | )); 55 | } 56 | 57 | module.exports = { 58 | #exports# 59 | } 60 | `; 61 | 62 | const topic_publisher_snipped_code = `#topic#: { 63 | publish: async (data) => { 64 | topic_handler('#topic#', data); 65 | } 66 | }`; 67 | 68 | const cron_retry_failed_processors_code = `const { CronJob } = require('cron'); 69 | const { #topics-table-module# } = require('lessSqliteDB'); 70 | 71 | const time_zone = Intl.DateTimeFormat().resolvedOptions().timeZone; 72 | 73 | const process_topic_queue = async (data, db_client) => { 74 | const topic_name = data.topic; 75 | const processor_name = data.processor; 76 | 77 | try { 78 | const topic_data = JSON.parse(data.message); 79 | 80 | const processor = require(\`@chuva.io/less/topics/handlers/\${topic_name}/\${processor_name}\`); 81 | 82 | await processor.process(topic_data); 83 | await db_client.delete({ id: data.id }); 84 | 85 | console.log( 86 | \`#less-local-info-tag#Processor "\${processor_name}" from topic "\${topic_name}" was successfully processed after \${data.times_retried + 1} retries.#less-local-info-tag#\` 87 | ); 88 | } catch(error) { 89 | console.log( 90 | \`#less-local-error-tag#Processor "\${processor_name}" from topic "\${topic_name}" failed to process after \${data.times_retried + 1} retries. Error: <#\${error}#>#less-local-error-tag#\` 91 | ); 92 | 93 | await db_client.update({ 94 | columns: { 95 | retrying: false, 96 | times_retried: data.times_retried + 1 97 | }, 98 | where: { 99 | id: data.id 100 | } 101 | }); 102 | } 103 | }; 104 | 105 | const cron = new CronJob( 106 | '*/5 * * * * *', 107 | async () => { 108 | const client = new #topics-table-module#(); 109 | const items = await client.getAll({ retrying: false }); 110 | 111 | if (items.length) { 112 | await client.update({ 113 | columns: { retrying: true }, 114 | where: { 115 | id: { 'in': items.map(item => item.id) } 116 | } 117 | }); 118 | 119 | await Promise.all(items.map(item => process_topic_queue(item, client))); 120 | } 121 | 122 | client.close(); 123 | }, 124 | null, 125 | false, 126 | time_zone 127 | ); 128 | 129 | module.exports = () => { 130 | cron.start(); 131 | }; 132 | `; 133 | 134 | const build_topics = (config) => { 135 | const topics_handlers_name = 'handlers'; 136 | const project_topics_path = path.resolve( 137 | config.project_less_resources_location, 138 | config.less_resources.topics 139 | ); 140 | 141 | if (!fs.existsSync(project_topics_path)) { 142 | return; 143 | } 144 | 145 | const topics_handlers = map_dirs_recursive(project_topics_path); 146 | const topics_path = path.resolve( 147 | config.chuva_dependency_path, 148 | config.less_resources.topics 149 | ); 150 | 151 | if (!fs.existsSync(topics_path)) { 152 | fs.mkdirSync(topics_path, { recursive: true }); 153 | } 154 | 155 | const module_exports = Object 156 | .keys(topics_handlers) 157 | .map( 158 | element => topic_publisher_snipped_code 159 | .replaceAll('#topic#', element) 160 | ).join(',\n '); 161 | 162 | const handlers_path = path.resolve( 163 | topics_path, 164 | topics_handlers_name 165 | ); 166 | 167 | fs.mkdirSync(handlers_path, { recursive: true }); 168 | fs.writeFileSync( 169 | topics_path + '/index.js', 170 | publishers_code 171 | .replace('#exports#', module_exports) 172 | .replaceAll('#topics-handlers#', topics_handlers_name) 173 | .replaceAll('#less-local-info-tag#', config.less_local_info_flag) 174 | .replaceAll('#less-local-error-tag#', config.less_local_error_flag) 175 | .replaceAll('#database-dependency#', config.sqlite_database_dependency) 176 | .replaceAll('#topics-table-module#', config.less_sqlite_tables.topics.model) 177 | ); 178 | 179 | fs.cpSync( 180 | path.join( 181 | config.project_less_resources_location, 182 | config.less_resources.topics 183 | ), 184 | handlers_path, 185 | { recursive: true } 186 | ); 187 | 188 | const cron_retry_failed_topic_processors_name = 'cron_retry_failed_topic_processors'; 189 | const cron_retry_failed_processors_path = path.resolve( 190 | config.project_build_path, 191 | cron_retry_failed_topic_processors_name 192 | ); 193 | 194 | config.app_imports += `const ${cron_retry_failed_topic_processors_name} = require(\'./${cron_retry_failed_topic_processors_name}\');\n`; 195 | config.app_callers += `${cron_retry_failed_topic_processors_name}();\n`; 196 | 197 | fs.mkdirSync(cron_retry_failed_processors_path, { recursive: true }); 198 | fs.writeFileSync( 199 | cron_retry_failed_processors_path + '/index.js', 200 | cron_retry_failed_processors_code 201 | .replaceAll('#less-local-info-tag#', config.less_local_info_flag) 202 | .replaceAll('#less-local-error-tag#', config.less_local_error_flag) 203 | .replaceAll('#topics-table-module#', config.less_sqlite_tables.topics.model) 204 | ); 205 | }; 206 | 207 | export default build_topics; -------------------------------------------------------------------------------- /commands/local/helpers/check_resources/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import map_dirs_recursive from '../map_dirs_recursive/index.js'; 4 | 5 | const find_languges_file = (item) => { 6 | if (item && Object.keys(item).find(el => /^__init__\.py|index\.js$/.test(el))) { 7 | return true; 8 | } 9 | 10 | if (item === null) { 11 | return false; 12 | } 13 | 14 | return Boolean(Object.keys(item).find(el => find_languges_file(item[el]))); 15 | } 16 | 17 | const check_resources = (project_less_resources_location, less_resources) => { 18 | const resources = fs.readdirSync(project_less_resources_location); 19 | 20 | let found; 21 | resources 22 | .filter( 23 | item => Object.keys(less_resources).includes(item) 24 | ).forEach(resource => { 25 | found = { 26 | ...(found || {}), 27 | [resource]: find_languges_file(map_dirs_recursive( 28 | path.resolve( 29 | project_less_resources_location, 30 | resource 31 | ) 32 | )) 33 | }; 34 | }); 35 | 36 | return found; 37 | }; 38 | 39 | export default check_resources; -------------------------------------------------------------------------------- /commands/local/helpers/convert_snake_to_camel_case/index.js: -------------------------------------------------------------------------------- 1 | const convert_snake_to_camel_case = (str) => { 2 | const words = str.split('_'); 3 | 4 | const camel_case = words.map(word => `${word[0].toUpperCase()}${word.substring(1)}`); 5 | 6 | return camel_case.join(''); 7 | } 8 | 9 | export default convert_snake_to_camel_case -------------------------------------------------------------------------------- /commands/local/helpers/create_dotenv_based_on_less_config/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import chalk from 'chalk'; 4 | 5 | import readYamlFile from 'read-yaml-file'; 6 | import add_to_package_json from '../add_to_package_json/index.js'; 7 | 8 | import { LESS_LOCAL_FLAG } from '../../constants/index.js' 9 | 10 | const create_dotenv_based_on_less_config = async (config) => { 11 | const less_config_path = path.join(config.project_location, 'less.config'); 12 | 13 | if (!fs.existsSync(less_config_path)) { 14 | return; 15 | } 16 | 17 | const less_config_env_vars = await readYamlFile(less_config_path); 18 | 19 | const env_file = less_config_env_vars.env_vars.map( 20 | env_var => { 21 | if (!process.env[env_var]) { 22 | const less_local_flag = chalk.yellowBright(LESS_LOCAL_FLAG); 23 | process.exitCode = 1; 24 | 25 | console.log( 26 | less_local_flag, 27 | chalk.redBright(`Error: Could not find the env_var "${env_var}" exported to your terminal.`) 28 | ); 29 | 30 | throw Error('env var not found'); 31 | } 32 | 33 | return `${env_var}=${process.env[env_var]}` 34 | } 35 | ).join('\n'); 36 | 37 | fs.mkdirSync(config.project_build_path, { recursive: true }); 38 | fs.writeFileSync( 39 | path.join(config.project_build_path, '.env'), 40 | env_file 41 | ); 42 | 43 | add_to_package_json(config, { 44 | dependencies: { 45 | dotenv: "^16.4.5" 46 | } 47 | }); 48 | 49 | config.app_imports = 50 | `require(\'dotenv\').config({ path: '${config.project_build_path}/.env' });\n` 51 | + config.app_imports; 52 | } 53 | 54 | export default create_dotenv_based_on_less_config; 55 | -------------------------------------------------------------------------------- /commands/local/helpers/get_yarn_path/index.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { fileURLToPath } from 'url'; 3 | 4 | export default () => { 5 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 6 | const yarn_path = path.resolve(__dirname, '../../../..', 'node_modules', '.bin', 'yarn'); 7 | 8 | return yarn_path; 9 | } -------------------------------------------------------------------------------- /commands/local/helpers/less_app_config_file/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { APP_CONFIG_FILE } from '../../constants/index.js'; 4 | 5 | const get = (build_path) => { 6 | const config_file_path = path.resolve(build_path, APP_CONFIG_FILE); 7 | 8 | if (!fs.existsSync(config_file_path)) { 9 | return {}; 10 | } 11 | 12 | return JSON.parse(fs.readFileSync(config_file_path, 'utf-8')); 13 | } 14 | 15 | const set = (build_path, new_data) => { 16 | const config_file_path = path.resolve(build_path, APP_CONFIG_FILE); 17 | 18 | let data = {}; 19 | if (fs.existsSync(config_file_path)) { 20 | data = JSON.parse(fs.readFileSync(config_file_path, 'utf-8')); 21 | } 22 | 23 | Object.keys(new_data).forEach(key => { 24 | data[key] = new_data[key]; 25 | }); 26 | 27 | fs.writeFileSync(config_file_path, JSON.stringify(data, null, 2)); 28 | }; 29 | 30 | export default { 31 | get, 32 | set 33 | }; -------------------------------------------------------------------------------- /commands/local/helpers/map_dirs_recursive/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | const map_dirs_recursive = (dir_path) => { 5 | if (!fs.statSync(dir_path).isDirectory()) { 6 | return null; 7 | } 8 | 9 | const path_dirs = fs.readdirSync(dir_path); 10 | 11 | const dirs = {}; 12 | path_dirs.forEach(item => { 13 | const new_path = path.join(dir_path, item); 14 | 15 | const result = map_dirs_recursive(new_path); 16 | 17 | dirs[item] = result; 18 | }); 19 | 20 | return dirs; 21 | } 22 | 23 | export default map_dirs_recursive; -------------------------------------------------------------------------------- /commands/local/helpers/run_app/index.js: -------------------------------------------------------------------------------- 1 | import ora from 'ora'; 2 | import chalk from 'chalk'; 3 | import { spawn } from 'child_process'; 4 | 5 | import { LESS_LOCAL_FLAG } from '../../constants/index.js'; 6 | import less_app_config_file from '../less_app_config_file/index.js'; 7 | 8 | const run_app = async (config) => { 9 | const less_local_flag = chalk.yellowBright(LESS_LOCAL_FLAG); 10 | const app_config = less_app_config_file.get(config.project_build_path); 11 | 12 | const spinner = ora(chalk.gray(LESS_LOCAL_FLAG + 'Running app...')); 13 | spinner.start(); 14 | await new Promise((resolve) => { 15 | const app = spawn('ts-node', ['app.js'], { cwd: config.project_build_path }); 16 | 17 | app.stdout.on('data', (data) => { 18 | data = data.toString('utf-8').slice(0, -1); 19 | 20 | if (data.includes(app_config.app_running_flag)) { 21 | spinner.stop(); 22 | console.log(less_local_flag, chalk.greenBright(`App "${config.project_build_path.split('/').pop()}" is running ✅`)) 23 | console.log(less_local_flag, chalk.greenBright('Resources:')); 24 | if (Object.keys(app_config.apis).length) { 25 | console.log(less_local_flag, chalk.greenBright('\tList of APIs:')); 26 | Object.keys(app_config.apis).forEach(api => { 27 | console.log(less_local_flag, chalk.greenBright(`\t\t- ${api}: http://localhost:${app_config.apis[api].port}`)); 28 | }); 29 | } 30 | 31 | if (Object.keys(app_config.sockets).length) { 32 | console.log(less_local_flag); 33 | console.log(less_local_flag, chalk.greenBright('\tList of Sockets:')); 34 | Object.keys(app_config.sockets).forEach(socket => { 35 | console.log(less_local_flag, chalk.greenBright(`\t\t- ${socket}: ws://localhost:${app_config.sockets[socket].port}`)); 36 | }); 37 | } 38 | 39 | console.log(less_local_flag, '🇨🇻\n\n'); 40 | return; 41 | } 42 | 43 | if (data.includes(config.less_local_error_flag) || data.includes(config.less_local_info_flag)) { 44 | let data_string = data; 45 | const errors = data_string.match( 46 | new RegExp(`${config.less_local_error_flag}.*${config.less_local_error_flag}`, 'g') 47 | ); 48 | 49 | const infos = data_string.match( 50 | new RegExp(`${config.less_local_info_flag}.*${config.less_local_info_flag}`, 'g') 51 | ); 52 | 53 | if (errors) { 54 | errors.forEach(error => { 55 | data_string = data_string.replaceAll(error, ''); 56 | const error_message = error.match(/<\#(.*)\#>/)[1]; 57 | error = error.replace(error_message, ''); 58 | 59 | console.error(chalk.redBright( 60 | error 61 | .replace('<##>', '') 62 | .replace(error_message, '') 63 | .replaceAll(config.less_local_error_flag, '') 64 | )); 65 | console.error(error_message); 66 | }); 67 | } 68 | 69 | if (infos) { 70 | infos.forEach(info => { 71 | data_string = data_string.replaceAll(info, ''); 72 | console.info(chalk.greenBright(info.replaceAll(config.less_local_info_flag, ''))); 73 | }); 74 | } 75 | 76 | console.log(data_string); 77 | return; 78 | } 79 | 80 | console.log(data); 81 | }); 82 | 83 | app.stderr.on('data', (data) => { 84 | console.error(chalk.redBright('Error on app execution:'), data.toString('utf-8')); 85 | }); 86 | 87 | app.on('close', (code) => { 88 | console.log(`child process exited with code ${code}`); 89 | resolve() 90 | }); 91 | 92 | }); 93 | } 94 | 95 | export default run_app; -------------------------------------------------------------------------------- /commands/local/list_projects/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import ora from 'ora'; 4 | import chalk from 'chalk'; 5 | import { get as get_build_path } from '../helpers/build_path/index.js'; 6 | 7 | import { LESS_LOCAL_FLAG } from '../constants/index.js'; 8 | import less_app_config_file from '../helpers/less_app_config_file/index.js'; 9 | 10 | const list_apps = () => { 11 | const builts_path = get_build_path(); 12 | 13 | const spinner = ora(chalk.gray(`${LESS_LOCAL_FLAG} Listing built projects...`)); 14 | spinner.start(); 15 | 16 | const less_local_flag = chalk.yellowBright(LESS_LOCAL_FLAG); 17 | 18 | let apps = fs.readdirSync(builts_path); 19 | apps = apps.filter(app => fs.statSync(builts_path + `/${app}`).isDirectory()); 20 | 21 | if (apps.length === 0) { 22 | console.log( 23 | less_local_flag, 24 | chalk.greenBright('There are no built projects.') 25 | ); 26 | 27 | return; 28 | } 29 | spinner.stop(); 30 | apps.forEach(app => { 31 | const app_info = less_app_config_file.get(path.join(builts_path, app)); 32 | 33 | console.log(chalk.bold.greenBright('ID:'), chalk.cyanBright(app)); 34 | console.log(chalk.bold.greenBright('Created At:'), chalk.cyanBright(app_info.created_at)); 35 | console.log(chalk.bold.greenBright('Updated At:'), chalk.cyanBright(app_info.updated_at), '\n'); 36 | }); 37 | }; 38 | 39 | export default list_apps; -------------------------------------------------------------------------------- /commands/local/run_app/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import chalk from 'chalk'; 4 | import run_app_helper from '../helpers/run_app/index.js'; 5 | import { get as get_build_path } from '../helpers/build_path/index.js'; 6 | 7 | import { LESS_LOCAL_FLAG, LESS_LOCAL_INFO_FLAG, LESS_LOCAL_ERROR_FLAG } from '../constants/index.js'; 8 | 9 | const run_app = async (project_name) => { 10 | const project_build_path = path.join( 11 | get_build_path(), 12 | project_name 13 | ); 14 | 15 | const less_local_flag = chalk.yellowBright(LESS_LOCAL_FLAG); 16 | if (!fs.existsSync(project_build_path)) { 17 | console.log( 18 | less_local_flag, 19 | chalk.redBright(`The project_name "${project_name}" does not exist.`) 20 | ); 21 | process.exitCode = 1; 22 | return; 23 | } 24 | const config = { 25 | project_build_path, 26 | less_local_info_flag: LESS_LOCAL_INFO_FLAG, 27 | less_local_error_flag: LESS_LOCAL_ERROR_FLAG 28 | }; 29 | 30 | await run_app_helper(config); 31 | }; 32 | 33 | export default run_app; -------------------------------------------------------------------------------- /commands/login_profile/get_login_profile.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import chalk from 'chalk'; 3 | 4 | import { get_less_token, verify_auth_token } from '../helpers/credentials.js'; 5 | import create_server_url from '../helpers/create_server_url.js'; 6 | 7 | export default async function get_login_profile(_, command) { 8 | const organization_id = command.parent.opts().organization; 9 | await verify_auth_token(); 10 | 11 | const serverUrl = create_server_url(organization_id, 'aws-login-profiles'); 12 | 13 | try { 14 | const LESS_TOKEN = await get_less_token(); 15 | const headers = { 16 | Authorization: `Bearer ${LESS_TOKEN}` 17 | }; 18 | 19 | const response = await axios.get(serverUrl, { headers }); 20 | 21 | if (response.status === 200) { 22 | const credentials = response.data 23 | console.log(chalk.bold.greenBright('IAM username:'), chalk.whiteBright(credentials.user_name)); 24 | console.log(chalk.bold.greenBright('Password:'), chalk.whiteBright(credentials.password)); 25 | console.log(chalk.bold.greenBright('AWS access portal URL:'), chalk.whiteBright(credentials.iam_login_url)); 26 | 27 | } 28 | } catch (error) { 29 | console.error(chalk.redBright('Error:'), error?.response?.data?.error || 'Get login profile failed'); 30 | process.exitCode = 1; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /commands/projects/delete.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { verify_auth_token, get_less_token } from '../helpers/credentials.js'; 3 | import validate_project_name from '../helpers/validations/validate_project_name.js'; 4 | import create_server_url from '../helpers/create_server_url.js'; 5 | import axios from 'axios'; 6 | 7 | export default async function delete_project(organization_id, project_name) { 8 | validate_project_name(project_name); 9 | await verify_auth_token(); 10 | const serverUrl = create_server_url(organization_id, `projects/${project_name}`); 11 | 12 | try { 13 | const LESS_TOKEN = await get_less_token(); 14 | const headers = { 15 | Authorization: `Bearer ${LESS_TOKEN}` 16 | }; 17 | 18 | const response = await axios.delete(serverUrl, { headers }); 19 | 20 | if (response.status === 202) { 21 | console.log(chalk.yellowBright('[less-cli]'), chalk.bold.greenBright(response.data.message)); 22 | } 23 | } catch (error) { 24 | console.error(chalk.redBright('Error:'), error?.response?.data || 'Delete project failed'); 25 | process.exitCode = 1; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /commands/projects/get_all.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import chalk from 'chalk'; 3 | import create_api_url from '../helpers/create_api_url.js'; 4 | import { verify_auth_token, get_less_token } from '../helpers/credentials.js'; 5 | import Table from 'cli-table3'; 6 | 7 | export default async function get_all(organization_id) { 8 | await verify_auth_token(); 9 | 10 | const apiUrl = create_api_url(organization_id, 'projects'); 11 | 12 | try { 13 | const LESS_TOKEN = await get_less_token(); 14 | const headers = { 15 | Authorization: `Bearer ${LESS_TOKEN}` 16 | }; 17 | 18 | const response = await axios.get(apiUrl, { headers }); 19 | if (response.status === 200) { 20 | // Prepare table for CLI output 21 | 22 | // Prepare the table headers 23 | const headers = [ 24 | "Project Name", 25 | "Created", 26 | "Updated", 27 | "Status" 28 | ].map(title => chalk.bold.greenBright(title)); // Set header colors 29 | 30 | const table = new Table({ 31 | head: headers 32 | }); 33 | 34 | // Set table colors for each item 35 | table.push( 36 | ...response.data.data 37 | .map(item => ([ 38 | item.id, 39 | item.created_at, 40 | item.updated_at, 41 | item.status 42 | ] 43 | .map(item => chalk.cyanBright(item)) // Set item colors 44 | )) 45 | ); 46 | 47 | console.log(table.toString()); 48 | } 49 | } catch (error) { 50 | console.error(chalk.redBright('Error:'), error?.response?.data?.error || 'Get projects failed'); 51 | process.exitCode = 1; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /commands/projects/get_by_id.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import chalk from 'chalk'; 3 | import { verify_auth_token, get_less_token } from '../helpers/credentials.js'; 4 | import create_server_url from '../helpers/create_server_url.js'; 5 | 6 | export default async function get_by_id(project_name, _, command) { 7 | const organization_id = command.parent.opts().organization; 8 | if (!/^[a-zA-Z][-a-zA-Z0-9]*$/.test(project_name)) { 9 | console.log(chalk.redBright('Error:'), 'The project_name must satisfy regular expression pattern: [a-zA-Z][-a-zA-Z0-9]'); 10 | process.exitCode = 1; 11 | return ; 12 | } 13 | 14 | await verify_auth_token(); 15 | const serverUrl = create_server_url(organization_id, `projects/${project_name}`); 16 | 17 | try { 18 | const LESS_TOKEN = await get_less_token(); 19 | const headers = { 20 | Authorization: `Bearer ${LESS_TOKEN}` 21 | }; 22 | 23 | const response = await axios.get(serverUrl, { headers }); 24 | 25 | if (response.status === 200) { 26 | if (response.data.apis?.length) { 27 | console.log(chalk.yellowBright('[less-cli]'), chalk.bold.greenBright('API URLs')); 28 | response.data.apis.forEach(api => { 29 | console.log(chalk.yellowBright('[less-cli]'), chalk.cyanBright(`\t- ${api.api_name}: ${api.url}`)); 30 | }); 31 | } 32 | 33 | if (response.data.websockets?.length) { 34 | console.log(chalk.yellowBright('[less-cli]'), chalk.bold.greenBright('WEBSOCKET URLs')); 35 | response.data.websockets.forEach(websocket => { 36 | console.log(chalk.yellowBright('[less-cli]'), chalk.cyanBright(`\t- ${websocket.api_name}: ${websocket.url}`)); 37 | }); 38 | } 39 | } 40 | } catch (error) { 41 | console.error(chalk.redBright('Error:'), error?.response?.data?.error || 'Get projects failed'); 42 | process.exitCode = 1; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /commands/projects/logs/fetch_and_log_function_logs.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { verify_auth_token, get_less_token } from '../../helpers/credentials.js'; 3 | import handle_error from '../../helpers/handle_error.js'; 4 | import create_server_url from '../../helpers/create_server_url.js'; 5 | import axios from 'axios'; 6 | 7 | /** 8 | * Validates the options object to ensure it contains the required properties. 9 | * @param {Object} options - The options object. 10 | * @param {string} options.project - The project name. 11 | * @param {string} options.function - The path to the resource. 12 | */ 13 | function validateOptions(options) { 14 | const { project, function: function_id } = options; 15 | 16 | if (!project) { 17 | handle_error('Project is required'); 18 | } 19 | 20 | if (!function_id) { 21 | handle_error('Function is required'); 22 | } 23 | } 24 | 25 | /** 26 | * Fetches and logs the function logs for a given project and resource path. 27 | * @param {Object} options - The options object. 28 | * @param {string} options.project - The project name. 29 | * @param {string} options.function - The path of the resource. 30 | */ 31 | export default async function fetch_and_log_function_logs(options, command) { 32 | const organization_id = command.parent.opts().organization; 33 | validateOptions(options); 34 | 35 | const { project, function: function_id } = options; 36 | await verify_auth_token(); 37 | 38 | try { 39 | const LESS_TOKEN = await get_less_token(); 40 | const headers = { 41 | Authorization: `Bearer ${LESS_TOKEN}` 42 | }; 43 | 44 | const resource_id = function_id 45 | .replace(/^\//, '') 46 | .replace(/.*?less\//, '') 47 | .replace(/\/(index\.js|__init__\.py)$/, '') 48 | .replace(/\.(js|py)$/, ''); 49 | 50 | const serverUrl = create_server_url(organization_id, `projects/${project}/resources/${encodeURIComponent(resource_id)}/logs`); 51 | const { status, data } = await axios.get(serverUrl, { headers }); 52 | 53 | if (status === 200) { 54 | if (!data || data.length === 0) { 55 | handle_error(`No logs found for the function ${function_id} in project ${project}`); 56 | } 57 | 58 | data.forEach(log => { 59 | console.log(chalk.bold.greenBright(log.timestamp), log.message.slice(0, -1).replaceAll('\t',' ')); 60 | }); 61 | } 62 | } catch (error) { 63 | handle_error(error?.response?.data?.error || `Get logs for the function ${function_id} in project ${project} failed`); 64 | process.exitCode = 1; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /commands/service/api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import CONFIG from '../../utils/config.js'; 3 | 4 | const api = axios.create({ 5 | baseURL: CONFIG.LESS_SERVER_BASE_URL 6 | }); 7 | 8 | export default api; 9 | -------------------------------------------------------------------------------- /commands/user/create_account.js: -------------------------------------------------------------------------------- 1 | import inquirer from 'inquirer'; 2 | import chalk from 'chalk'; 3 | import api from '../service/api.js' 4 | 5 | const questions = [ 6 | { 7 | type: 'input', 8 | name: 'name', 9 | message: 'Enter your name:', 10 | validate: (input) => (input.trim() !== '' ? true : 'Name is required.'), 11 | }, 12 | { 13 | type: 'input', 14 | name: 'email', 15 | message: 'Enter your email:', 16 | validate: (input) => 17 | /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input) ? true : 'Please enter a valid email address.', 18 | }, 19 | { 20 | type: 'password', 21 | name: 'password', 22 | message: 'Enter your password:', 23 | mask: '*', 24 | validate: (input) => { 25 | if (input.trim() === '') return 'Password is required.'; 26 | if (input.length < 8) return 'Password must be at least 8 characters long.'; 27 | if (!/[A-Z]/.test(input)) return 'Password must contain at least one uppercase letter.'; 28 | if (!/[a-z]/.test(input)) return 'Password must contain at least one lowercase letter.'; 29 | if (!/[0-9]/.test(input)) return 'Password must contain at least one number.'; 30 | if (!/[^A-Za-z0-9]/.test(input)) return 'Password must contain at least one special symbol.'; 31 | return true; 32 | }, 33 | } 34 | ]; 35 | 36 | async function create(user) { 37 | try { 38 | const response = await api.post('v1/users', user); 39 | 40 | if (response.status === 201) { 41 | await inquirer 42 | .prompt([ 43 | { 44 | type: 'password', 45 | name: 'verificationCode', 46 | mask: '*', 47 | message: 'Enter the verification code sent to your email:', 48 | validate: async input => await verify_user(input, response.data.id) 49 | }, 50 | ]); 51 | 52 | console.log(chalk.yellowBright('[less-cli]'), chalk.green('Account verified!')); 53 | } 54 | 55 | } catch (error) { 56 | if (error.response && error.response.status === 400) { 57 | console.log(chalk.redBright('Error:'), error.response.data.error); 58 | } else { 59 | console.error(chalk.redBright('Error:'), error.message || 'An error occurred'); 60 | } 61 | } 62 | } 63 | 64 | async function verify_user(code, user_id) { 65 | try { 66 | await api.post(`v1/users/${user_id}/verify`, { code }); 67 | return true; 68 | } catch (error) { 69 | if (error.response && error.response.status === 400) { 70 | return error.response.data.error; 71 | } 72 | 73 | console.error(chalk.redBright('\nError:'), error.message || 'An error occurred'); 74 | } 75 | } 76 | 77 | export default async function create_account() { 78 | const answers = await inquirer.prompt(questions); 79 | await create(answers) 80 | } 81 | -------------------------------------------------------------------------------- /commands/user/create_session.js: -------------------------------------------------------------------------------- 1 | import inquirer from 'inquirer'; 2 | import chalk from 'chalk'; 3 | import api from '../service/api.js' 4 | import { 5 | set_credentials, 6 | } from '../helpers/credentials.js'; 7 | 8 | const questions = [ 9 | { 10 | type: 'input', 11 | name: 'email', 12 | message: 'Enter your email:', 13 | validate: (input) => 14 | /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input) ? true : 'Please enter a valid email address.', 15 | }, 16 | { 17 | type: 'password', 18 | name: 'password', 19 | message: 'Enter your password:', 20 | mask: '*', 21 | validate: (input) => { 22 | if (input.trim() === '') return 'Password is required.'; 23 | return true; 24 | }, 25 | } 26 | ]; 27 | 28 | async function login(user) { 29 | try { 30 | const response = await api.post('v1/sessions', user); 31 | 32 | if (response.status === 201) { 33 | await set_credentials({ 34 | LESS_TOKEN: response.data.token 35 | }); 36 | 37 | if (process.exitCode && process.exitCode !== 0) { 38 | return ; 39 | } 40 | 41 | console.log(chalk.yellowBright('[less-cli]'), chalk.green('Login successful! Your LESS_TOKEN has been exported to your environment.')); 42 | } 43 | 44 | } catch (error) { 45 | if (error.response && error.response.status === 401) { 46 | const forgot_password_command = chalk.yellowBright('less-cli forgot-password') 47 | console.log( 48 | chalk.redBright('Error:'), 49 | `${error.response.data.error}\n\nIf you have forgotten your password you can reset it by running the following command:\n - ${forgot_password_command}`); 50 | } else { 51 | console.error(chalk.redBright('Error:'), error.message || 'An error occurred'); 52 | } 53 | process.exitCode = 1; 54 | } 55 | } 56 | 57 | export default async function create_session() { 58 | const answers = await inquirer.prompt(questions); 59 | await login(answers); 60 | } 61 | -------------------------------------------------------------------------------- /commands/user/forgot_password.js: -------------------------------------------------------------------------------- 1 | import inquirer from 'inquirer'; 2 | import chalk from 'chalk'; 3 | import api from '../service/api.js'; 4 | 5 | /** 6 | * Validates an email address. 7 | * @param {string} input - The email address to validate. 8 | * @returns {(boolean|string)} True if the email is valid, otherwise a string with an error message. 9 | */ 10 | const email_validation = (input) => 11 | /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input) 12 | ? true 13 | : 'Please enter a valid email address.'; 14 | 15 | /** 16 | * Validates a password. 17 | * @param {string} input - The password to validate. 18 | * @returns {(boolean|string)} True if the password is valid, otherwise a string with an error message. 19 | */ 20 | const password_validation = (input) => { 21 | if (input.trim() === '') return 'Password is required.'; 22 | if (input.length < 8) return 'Password must be at least 8 characters long.'; 23 | if (!/[A-Z]/.test(input)) 24 | return 'Password must contain at least one uppercase letter.'; 25 | if (!/[a-z]/.test(input)) 26 | return 'Password must contain at least one lowercase letter.'; 27 | if (!/[0-9]/.test(input)) return 'Password must contain at least one number.'; 28 | if (!/[^A-Za-z0-9]/.test(input)) 29 | return 'Password must contain at least one special symbol.'; 30 | return true; 31 | }; 32 | 33 | /** 34 | * Resets the password for a user account. 35 | * @param {string} email - The email address of the user. 36 | * @param {string} new_password - The new password for the user. 37 | * @param {string} verification_code - The verification code sent to the user's email. 38 | * @returns {Promise} True if the password is reset successfully, otherwise an error message. 39 | */ 40 | async function reset_password(email, new_password, verification_code) { 41 | try { 42 | await api.post(`v1/confirm_password`, { 43 | email, 44 | new_password, 45 | verification_code, 46 | }); 47 | return true; 48 | } catch (error) { 49 | if (error.response && error.response.status === 400) { 50 | return error.response.data.error; 51 | } 52 | console.error( 53 | chalk.redBright('\nError:'), 54 | error.message || 'An error occurred' 55 | ); 56 | } 57 | } 58 | 59 | /** 60 | * Initiates the password recovery process. 61 | * @param {string} email - The email address of the user. 62 | */ 63 | async function recover(email) { 64 | try { 65 | const response = await api.post('v1/forgot_password', { email }); 66 | 67 | if (response.status === 201) { 68 | const answers = await inquirer.prompt([ 69 | { 70 | type: 'password', 71 | name: 'verificationCode', 72 | mask: '*', 73 | message: 'Enter the verification code sent to your email:', 74 | validate: (input) => { 75 | if (input.trim() === '') return 'Verification code is required.'; 76 | if (input.length < 6) 77 | return 'Verification code must be at least 6 characters long.'; 78 | return true; 79 | }, 80 | }, 81 | { 82 | type: 'password', 83 | name: 'newPassword', 84 | message: 'Enter new password:', 85 | mask: '*', 86 | validate: password_validation, 87 | }, 88 | ]); 89 | 90 | await reset_password( 91 | email, 92 | answers.newPassword, 93 | answers.verificationCode 94 | ); 95 | console.log( 96 | chalk.yellowBright('[less-cli]'), 97 | chalk.green('Password reset successfully') 98 | ); 99 | } 100 | } catch (error) { 101 | if (error.response && error.response.status === 400) { 102 | console.log(chalk.redBright('Error:'), error.response.data.error); 103 | } else { 104 | console.error( 105 | chalk.redBright('Error:'), 106 | error.message || 'An error occurred' 107 | ); 108 | } 109 | process.exitCode = 1; 110 | } 111 | } 112 | 113 | /** 114 | * Initiates the password recovery process by prompting the user for their email. 115 | */ 116 | async function forgot_password() { 117 | const { email } = await inquirer.prompt([ 118 | { 119 | type: 'input', 120 | name: 'email', 121 | message: 'Enter your email:', 122 | validate: email_validation, 123 | }, 124 | ]); 125 | 126 | await recover(email); 127 | } 128 | 129 | export default forgot_password; 130 | -------------------------------------------------------------------------------- /commands/view_user_profile.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import chalk from 'chalk'; 3 | import { verify_auth_token, get_less_token } from './helpers/credentials.js'; 4 | import config from '../utils/config.js'; 5 | import Table from 'cli-table3'; 6 | 7 | export default async function get_all(organization_id) { 8 | await verify_auth_token(); 9 | 10 | const apiUrl = `${config.LESS_API_BASE_URL}/v1/users/me`; 11 | 12 | try { 13 | const LESS_TOKEN = await get_less_token(); 14 | const headers = { 15 | Authorization: `Bearer ${LESS_TOKEN}` 16 | }; 17 | 18 | const response = await axios.get(apiUrl, { headers }); 19 | if (response.status === 200) { 20 | // Prepare table for CLI output 21 | 22 | // Prepare the table headers 23 | const headers = [ 24 | "Name", 25 | "Email", 26 | ].map(title => chalk.bold.greenBright(title)); // Set header colors 27 | 28 | const table = new Table({ 29 | head: headers 30 | }); 31 | 32 | // Set table colors for each item 33 | const user = response.data; 34 | table.push([ 35 | user.name, 36 | user.email 37 | ] 38 | .map(item => chalk.cyanBright(item)) // Set item colors) 39 | ); 40 | 41 | console.log(table.toString()); 42 | } 43 | } catch (error) { 44 | console.error(chalk.redBright('Error:'), error?.response?.data?.error || 'Get projects failed'); 45 | console.error(chalk.redBright('Error:'), error); 46 | process.exitCode = 1; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@chuva.io/less-cli", 3 | "version": "1.0.0-beta.51", 4 | "description": "`less-cli` is a CLI tool that allows you to deploy your Less projects to AWS while providing several other tools to facilitate your interaction with Less.", 5 | "author": "Chuva, LLC", 6 | "license": "Apache-2.0", 7 | "url": "https://github.com/chuva-io/less-cli.git", 8 | "homepage": "https://docs.less.chuva.io", 9 | "type": "module", 10 | "exports": "./bin/index.js", 11 | "bin": { 12 | "less-cli": "bin/index.js" 13 | }, 14 | "engines": { 15 | "node": ">=20.x" 16 | }, 17 | "keywords": [ 18 | "distributed", 19 | "infinite scale", 20 | "event-driven", 21 | "events", 22 | "deploy", 23 | "realtime", 24 | "fault tolerant", 25 | "serverless", 26 | "rest api", 27 | "websocket", 28 | "microservice", 29 | "cloud", 30 | "infrastructure", 31 | "pub/sub", 32 | "aws", 33 | "python", 34 | "javascript", 35 | "go", 36 | "c#", 37 | "rust", 38 | "less", 39 | "less-cli" 40 | ], 41 | "dependencies": { 42 | "adm-zip": "^0.5.10", 43 | "axios": "^1.4.0", 44 | "chalk": "^5.3.0", 45 | "cli-table3": "0.6.5", 46 | "commander": "^11.0.0", 47 | "dotenv": "16.4.5", 48 | "glob": "^10.3.4", 49 | "inquirer": "^9.2.11", 50 | "js-yaml": "^4.1.0", 51 | "ora": "^7.0.1", 52 | "read-yaml-file": "2.1.0", 53 | "sqlite3": "5.1.7", 54 | "ws": "^8.13.0", 55 | "yarn": "1.22.22" 56 | }, 57 | "main": "bin/index.js", 58 | "bugs": { 59 | "url": "https://bitbucket.org/chuva-io/less-cli-client/issues" 60 | }, 61 | "scripts": { 62 | "test": "echo \"Error: no test specified\" && exit 1" 63 | }, 64 | "devDependencies": { 65 | "@less-ifc/types": "0.0.2" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /scripts/release/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # Function to log messages 6 | log() { 7 | echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')]: $@" 8 | } 9 | 10 | # Get version before merge 11 | OLD_VERSION=$(git describe --tags $(git rev-list --tags --max-count=1)) 12 | OLD_VERSION=${OLD_VERSION#v} # Remove 'v' prefix if it exists 13 | log "Old version: $OLD_VERSION" 14 | 15 | # Get version after merge 16 | NEW_VERSION=$(node -p "require('./package.json').version") 17 | log "New version: $NEW_VERSION" 18 | 19 | # Compare versions and publish if they are different 20 | if [ "$OLD_VERSION" != "$NEW_VERSION" ]; then 21 | log "Version changed from $OLD_VERSION to $NEW_VERSION. Publishing new version to npm..." 22 | 23 | # Set authentication details 24 | echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> .npmrc 25 | 26 | # Publish to npm 27 | yarn 28 | yarn publish --access=public 29 | 30 | # Create a new tag and release on GitHub 31 | git fetch --tags 32 | git tag -a "v$NEW_VERSION" -m "Release $NEW_VERSION" 33 | git push origin "v$NEW_VERSION" 34 | 35 | # Generate release notes from git commits 36 | RELEASE_NOTES=$(git log --pretty=format:"* %s" "v$OLD_VERSION".."v$NEW_VERSION") 37 | echo "Release Notes for $NEW_VERSION:" > RELEASE_NOTES.md 38 | echo "$RELEASE_NOTES" >> RELEASE_NOTES.md 39 | 40 | # Ensure GITHUB_TOKEN is used for authentication 41 | export GITHUB_TOKEN=$GH_TOKEN 42 | 43 | # Create the release using GitHub CLI 44 | gh release create "v$NEW_VERSION" --title "Release $NEW_VERSION" --notes-file RELEASE_NOTES.md 45 | 46 | else 47 | log "Version did not change." 48 | fi 49 | -------------------------------------------------------------------------------- /templates/ts/api/get/basic.ts: -------------------------------------------------------------------------------- 1 | import { Api } from '@less-ifc/types'; 2 | 3 | export const process: Api.Handler = async (request: Api.Request, response: Api.Response) => { 4 | response.body = "Hello world!"; 5 | return response; 6 | } 7 | -------------------------------------------------------------------------------- /templates/ts/api/get/with_logger_middleware.ts: -------------------------------------------------------------------------------- 1 | import { Api } from '@less-ifc/types'; 2 | 3 | // Example of a middleware function that logs requests 4 | const logger_middleware: Api.Middleware = async (request: Api.Request, response: Api.Response, next: Api.NextFunction) => { 5 | console.log('[LOGGER MIDDLEWARE] Processing request'); 6 | console.log('[LOGGER MIDDLEWARE] Request headers:', request.headers); 7 | console.log('[LOGGER MIDDLEWARE] Request path params:', request.params); 8 | console.log('[LOGGER MIDDLEWARE] Request query params:', request.query); 9 | console.log('[LOGGER MIDDLEWARE] Request body:', request.body); 10 | 11 | // You can also modify the request or response objects here if needed 12 | // For example, you can add a custom header to the response 13 | response.headers['X-Custom-Header'] = 'CustomValue'; 14 | 15 | // Or you can add the authenticated user to the request object 16 | request.user = { id: '001', name: 'Amilcar Cabral' }; 17 | 18 | // You can also return early from the middleware if you want to stop the request from reaching the main handler 19 | // Uncomment the line below to stop the request 20 | // return response; 21 | 22 | next(); 23 | } 24 | 25 | // Array of middleware functions to run before the main handler 26 | export const middlewares: Api.Middlewares = [ 27 | logger_middleware 28 | ]; 29 | 30 | // When someone accesses this API endpoint this function will be called. 31 | export const process: Api.Handler = async (request: Api.Request, response: Api.Response) => { 32 | /** 33 | * This contains the query params. 34 | * 35 | * Add "?status=sold" to the route to only return sold pets. 36 | */ 37 | const { status } = request.query; 38 | 39 | // Mock pets data. 40 | const pets = [ 41 | { id: 1, name: 'Dog', status: 'available' }, 42 | { id: 2, name: 'Cat', status: 'pending' }, 43 | { id: 3, name: 'Fish', status: 'sold' }, 44 | ] 45 | // Filter based on the `status` query parameter. 46 | .filter(pet => status ? pet.status === status : true); 47 | 48 | response.body = JSON.stringify(pets); 49 | 50 | return response; 51 | } 52 | -------------------------------------------------------------------------------- /templates/ts/cloud_function/basic.ts: -------------------------------------------------------------------------------- 1 | import { CloudFunction } from '@less-ifc/types'; 2 | 3 | export const process: CloudFunction.Handler = async (message: CloudFunction.Message) => { 4 | console.log('Cloud function triggered:', message); 5 | } 6 | -------------------------------------------------------------------------------- /templates/ts/cron/basic.ts: -------------------------------------------------------------------------------- 1 | import { Cron } from '@less-ifc/types'; 2 | 3 | export const process: Cron.Handler = async () => { 4 | console.log('Cron job triggered.'); 5 | } 6 | -------------------------------------------------------------------------------- /templates/ts/socket/channel/basic.ts: -------------------------------------------------------------------------------- 1 | import { Socket } from '@less-ifc/types'; 2 | 3 | export const process: Socket.ChannelHandler = async (message: Socket.ChannelMessage) => { 4 | console.log('Socket channel received message:', message); 5 | console.log('Connection ID:', message.connection_id); 6 | console.log('Payload:', message.data); 7 | } 8 | -------------------------------------------------------------------------------- /templates/ts/socket/connect/basic.ts: -------------------------------------------------------------------------------- 1 | import { Socket } from '@less-ifc/types'; 2 | 3 | export const process: Socket.ConnectionHandler = async (message: Socket.ConnectionMessage) => { 4 | console.log('Client connected to Socket:', message.connection_id); 5 | } 6 | -------------------------------------------------------------------------------- /templates/ts/socket/disconnect/basic.ts: -------------------------------------------------------------------------------- 1 | import { Socket } from '@less-ifc/types'; 2 | 3 | export const process: Socket.DisconnectionHandler = async (message: Socket.DisconnectionMessage) => { 4 | console.log('Client disconnected from Socket:', message.connection_id); 5 | } 6 | -------------------------------------------------------------------------------- /templates/ts/topic/subscriber/basic.ts: -------------------------------------------------------------------------------- 1 | import { Topic } from '@less-ifc/types'; 2 | 3 | export const process: Topic.Handler = async (message: Topic.Message) => { 4 | console.log('Received message from Topic:', message); 5 | } 6 | -------------------------------------------------------------------------------- /utils/check_for_updates.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { dirname } from 'path'; 4 | import { fileURLToPath } from 'url'; 5 | import axios from 'axios'; 6 | import chalk from 'chalk'; 7 | 8 | // Get __dirname in ES module 9 | const __dirname = dirname(fileURLToPath(import.meta.url)); 10 | 11 | // Get package version 12 | const package_path = path.join(__dirname, '..', 'package.json'); 13 | const package_content = JSON.parse(fs.readFileSync(package_path, 'utf-8')); 14 | const version = package_content?.version; 15 | 16 | // Function to fetch the latest version from npm 17 | async function check_for_updates() { 18 | const tag = 'https://api.github.com/repos/chuva-io/less-cli/tags' 19 | try { 20 | const response = await axios.get(tag); 21 | const latest_version = response.data[0].name.replace('v', ''); 22 | 23 | if (latest_version !== version) { 24 | console.log(chalk.yellowBright(`WARNING: You are using version ${version} of the Less CLI but the latest version is ${latest_version}.\nPlease update to have the best experience possible and take advantage of the latest features.`)); 25 | } 26 | } catch (error) { 27 | return ; 28 | } 29 | } 30 | 31 | export default check_for_updates; -------------------------------------------------------------------------------- /utils/config.js: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | 3 | if (process.env.NODE_ENV !== 'production') { 4 | dotenv.config(); 5 | } 6 | 7 | export default { 8 | LESS_SERVER_BASE_URL: process.env.LESS_SERVER_BASE_URL || "https://less-server.chuva.io", 9 | LESS_SERVER_SOCKET_URL: process.env.LESS_SERVER_SOCKET_URL || "wss://less-server.chuva.io", 10 | LESS_API_BASE_URL: process.env.LESS_API_BASE_URL || "https://9uztuf8i2e.execute-api.eu-west-1.amazonaws.com/less" 11 | }; 12 | -------------------------------------------------------------------------------- /utils/directory_has_less_folder.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | const directory_has_less_folder = () => { 5 | const project_dir_path = process.cwd(); 6 | const project_less_path = path.resolve(project_dir_path, 'less'); 7 | 8 | if (fs.existsSync(project_less_path)) { 9 | return fs.statSync(project_less_path).isDirectory(); 10 | } 11 | } 12 | 13 | export default directory_has_less_folder; 14 | -------------------------------------------------------------------------------- /utils/templates.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { fileURLToPath } from 'url'; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = path.dirname(__filename); 7 | 8 | export const api = { 9 | load_route_ts: () => fs.readFileSync(path.resolve(__dirname, '../templates/ts/api/get/basic.ts'), 'utf8'), 10 | }; 11 | 12 | export const socket = { 13 | load_connect_ts: () => fs.readFileSync(path.resolve(__dirname, '../templates/ts/socket/connect/basic.ts'), 'utf8'), 14 | load_disconnect_ts: () => fs.readFileSync(path.resolve(__dirname, '../templates/ts/socket/disconnect/basic.ts'), 'utf8'), 15 | load_channel_ts: () => fs.readFileSync(path.resolve(__dirname, '../templates/ts/socket/channel/basic.ts'), 'utf8'), 16 | }; 17 | 18 | export const cron = { 19 | load_cron_ts: () => fs.readFileSync(path.resolve(__dirname, '../templates/ts/cron/basic.ts'), 'utf8'), 20 | }; 21 | 22 | export const topic = { 23 | load_topic_subscriber_ts: () => fs.readFileSync(path.resolve(__dirname, '../templates/ts/topic/subscriber/basic.ts'), 'utf8'), 24 | }; 25 | 26 | export const cloud_function = { 27 | load_function_ts: () => fs.readFileSync(path.resolve(__dirname, '../templates/ts/cloud_function/basic.ts'), 'utf8'), 28 | }; 29 | --------------------------------------------------------------------------------