├── .formatter.exs ├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── SECURITY.md ├── guidance ├── guarded-struct.livemd ├── guarded-struct.md └── permission-access.md ├── lib ├── application.ex ├── helper │ ├── crypto.ex │ ├── extra.ex │ └── uuid.ex ├── mishka_developer_tools.ex └── modules │ ├── mnesia_assistant │ ├── error.ex │ ├── mnesia_assistant.ex │ ├── operation │ │ ├── backup_and_restore.ex │ │ ├── information.ex │ │ ├── query.ex │ │ ├── schema.ex │ │ ├── snmp.ex │ │ ├── table.ex │ │ └── transaction.ex │ └── pagination.ex │ ├── permission_access.ex │ └── queue_assistant.ex ├── mix.exs ├── mix.lock └── test ├── mishka_developer_tools_test.exs ├── permission_access_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 3 | ] 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [mishka-group] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: MishkaDeveloperTools CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | ci: 13 | env: 14 | GITHUB_ACTION: true 15 | 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | include: 20 | - pair: 21 | elixir: "1.17.1" 22 | otp: "27.0" 23 | 24 | runs-on: ubuntu-latest 25 | 26 | steps: 27 | - uses: actions/checkout@v4 28 | 29 | - uses: erlef/setup-beam@v1 30 | id: beam 31 | with: 32 | otp-version: ${{matrix.pair.otp}} 33 | elixir-version: ${{matrix.pair.elixir}} 34 | 35 | - name: Elixir and Erlang Dependencies 36 | run: | 37 | mix local.hex --force 38 | mix local.rebar --force 39 | 40 | - name: Source Compiling 41 | run: | 42 | mix deps.get 43 | mix deps.compile 44 | 45 | - name: Run test with temporary information 46 | run: | 47 | mix test --trace 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /_example 3 | /cover 4 | /deps 5 | /doc 6 | /.fetch 7 | erl_crash.dump 8 | *.ez 9 | *.beam 10 | /config/*.secret.exs 11 | .elixir_ls/ 12 | .elixir-tools/ 13 | 14 | # General 15 | .DS_Store 16 | Desktop.ini 17 | .AppleDouble 18 | .LSOverride 19 | .tool-versions 20 | name 21 | 22 | # custom 23 | /lib/example 24 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog for MishkaDeveloperTools 0.1.7 2 | 3 | > Kindly ensure that the macro is updated as quickly as feasible. This version includes a bug patch in the macro kernel that eliminates the issue of not being able to build in projects. 4 | 5 | In the past, it was possible to extend validation and sanitizer functions within the macro itself; however, this was a relatively insignificant addition that was ultimately overwritten. The same opportunity will now be available to you if you include environment in the project. 6 | 7 | ### For example: 8 | ```elixir 9 | Application.put_env(:guarded_struct, :validate_derive, [TestValidate, TestValidate2]) 10 | Application.put_env(:guarded_struct, :sanitize_derive, [TestSanitize, TestSanitize2]) 11 | 12 | # OR 13 | Application.put_env(:guarded_struct, :validate_derive, TestValidate) 14 | Application.put_env(:guarded_struct, :sanitize_derive, TestSanitize) 15 | ``` 16 | 17 | I offer my heartfelt apologies for the occurrence of this bug and express my desire to encounter less similar challenges in the future. 18 | 19 | # Changelog for MishkaDeveloperTools 0.1.6 20 | 21 | ### Features: 22 | 23 | - [x] Add `Crypto` helper module 24 | - [x] Add new optional dependencies and their wrappers [`nimble_totp`, `joken`, `jason`, `plug`, `bcrypt_elixir`, `pbkdf2_elixir`, `argon2_elixir`] 25 | - [x] Add a normal and basic `validated_password?` function 26 | - [x] Add basic elixir wrapper for erlang queue 27 | - [x] Add new validattion for erlang queue inside `guarded_struct` 28 | - [x] Add some helpers functions for erlang queue 29 | 30 | ### Improvement: 31 | 32 | - [x] Fix and Add compile time check is there geo url module or not 33 | - [x] Support Struct as the builder entry 34 | - [x] Improve some `dialyzer` warning 35 | - [x] General improvements for the new version of Mishka Installer - [Refactor and Rewriting the Mishka installer project with Erlang's built-in databases - 0.1.0](https://github.com/mishka-group/mishka_installer/pull/99) 36 | 37 | ### Extra: 38 | 39 | - [x] Update `ex_doc` dep 40 | - [x] Update Github `CI` 41 | - [x] Delete all ecto deps and custom macro and tests 42 | 43 | # Changelog for MishkaDeveloperTools 0.1.5 44 | 45 | --- 46 | 47 | > **The decision was made that this version will be a long-term version, and it will also include features that are several versions behind the existing version. However, because of the pressing issues with the builder's loading speed and the solution to those issues, it was decided to release this version sooner with fewer features than it had originally planned.** 48 | 49 | --- 50 | 51 | ### Features: 52 | 53 | - [x] Add `condition_field` fields inside `__information__` function 54 | - [x] Inside module derive, in nested struct we can call from `caller` 55 | - [x] Add `uuid` from `ecto` 56 | - [x] Add keys and enforce keys in `__information__` function 57 | - [x] Add some helpers like: `timestamp`, `validated_user?` and validation `username` 58 | 59 | ### Improvement: 60 | 61 | - [x] Speed problem in the derive section, and before this part of app V0.1.4, [#30](https://github.com/mishka-group/mishka_developer_tools/issues/30) 62 | - [x] Fix performance issue inside sanitizer and validation, when we are using external `deps` 63 | - [x] Fix `main_validator` and **halt** the error when we have errors inside `validator` and not load `main_validator` 64 | - [x] Add some information and helper to be compatible for Mnesia (need more in the future) 65 | - [x] Fix bug and Add `NaiveDateTime`, `DateTime`, `Date` struct to map parser 66 | 67 | ### Extra 68 | 69 | - [x] Mnesia wrapper for Elixir, [#28](https://github.com/mishka-group/mishka_developer_tools/issues/28) 70 | - [x] Add Erlang guard convertor for Elixir (simple helper function) 71 | - [x] Add `Mnesia` pagination (infinite_scroll, numerical) 72 | - [x] Add some helper to work with `Mnesia` data 73 | 74 | # Changelog for MishkaDeveloperTools 0.1.4 75 | 76 | ### Features: 77 | 78 | - [x] Support whole entries check derives for struct or structs (external module) (**More information**: https://github.com/mishka-group/mishka_developer_tools/issues/26) 79 | 80 | - [x] Support `derive` and `validator` on `conditional_field` macro as entries checker 81 | - [x] Support nested conditional fields (**More information**: https://github.com/mishka-group/mishka_developer_tools/issues/25) 82 | 83 | ```elixir 84 | guardedstruct do 85 | conditional_field(:actor, any()) do 86 | field(:actor, struct(), struct: Actor, derive: "validate(map, not_empty)") 87 | 88 | conditional_field(:actor, any(), 89 | structs: true, 90 | derive: "validate(list, not_empty, not_flatten_empty_item)" 91 | ) do 92 | field(:actor, struct(), struct: Actor, derive: "validate(map, not_empty)") 93 | 94 | field(:actor, String.t(), derive: "sanitize(tag=strip_tags) validate(url, max_len=160)") 95 | end 96 | 97 | field(:actor, String.t(), derive: "sanitize(tag=strip_tags) validate(url, max_len=160)") 98 | end 99 | end 100 | ``` 101 | 102 | ### Fixed bugs: 103 | 104 | - [x] Fix showing different errors when they accompany a conditional errors 105 | - [x] Fix short anonymous function warning in elixir 1.16 106 | - [x] Support pre-check derives inside conditional fields 107 | - [x] Normalize conditional fields errors 108 | - [x] Normalize validator errors 109 | - [x] Normalize errors `hint` 110 | - [x] Normalize `derives` errors 111 | - [x] Fix `dialyzer` warning 112 | - [x] Support derive in normal conditional field without validator 113 | 114 | ```elixir 115 | conditional_field(:id, String.t()) do 116 | field(:id, String.t(), derive: "sanitize(tag=strip_tags) validate(url, max_len=160)") 117 | field(:id, any(), derive: "sanitize(tag=strip_tags) validate(not_empty_string, uuid)") 118 | end 119 | ``` 120 | 121 | ### Docs 122 | 123 | - [x] Add LiveBook 124 | -------------------------------------------------------------------------------- /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 | # Mishka Elixir Developer Tools 2 | 3 | 4 | Buy Me A Coffee 5 | 6 | 7 | ## Low Maintenance Warning: 8 | 9 | > **This library is in low maintenance mode, which means the author is currently only responding to pull requests.** 10 | 11 | We tried to deliver a series of our client's [**CMS**](https://github.com/mishka-group/mishka-cms) built on [**Elixir**](https://elixir-lang.org/) at the start of the [**Mishka Group**](https://github.com/mishka-group) project, but we recently archived this open-source project and have yet to make plans to rework and expand it. This system was created using [**Phoenix**](https://www.phoenixframework.org/) and [**Phoenix LiveView**](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html). After a long period, a series of macros and functional modules emerged from this project and our other projects, which we are gradually publishing in this library. 12 | 13 | > **NOTICE**: Do not use the master branch; this library is under heavy development. Expect version `0.1.7`, and for using the new features, please wait until a new release is out. 14 | 15 | --- 16 | 17 | - ### [GuardedStruct](https://github.com/mishka-group/guarded_struct) 18 | 19 | **We recently separated the [GuardedStruct](https://github.com/mishka-group/guarded_struct) macro into a standalone library. Please visit its repository for more information.** 20 | 21 | > The creation of this macro will allow you to build `Structs` that provide you with a number of important options, including the following: 22 | > 23 | > 1. Validation 24 | > 2. Sanitizing 25 | > 3. Constructor 26 | > 4. It provides the capacity to operate in a nested style simultaneously. 27 | 28 | - ### [PermissionAccess](https://github.com/mishka-group/mishka_developer_tools/blob/master/guidance/permission-access.md) 29 | 30 | > Consider the scenario in which you are responsible for maintaining each user's access level in the database related to users. 31 | > **It is unix like way**. 32 | 33 | --- 34 | 35 | > **Mishka developer tools** provides some macros and modules to make creating your elixir application as easy as possible 36 | 37 | ## Installation 38 | 39 | The package can be installed by adding `mishka_developer_tools` to your list of dependencies in `mix.exs`: 40 | 41 | ```elixir 42 | def deps do 43 | [ 44 | {:mishka_developer_tools, "~> 0.1.7"} 45 | ] 46 | end 47 | ``` 48 | 49 | The docs can be found at [https://hexdocs.pm/mishka_developer_tools](https://hexdocs.pm/mishka_developer_tools). 50 | 51 | [![Run in Livebook](https://livebook.dev/badge/v1/pink.svg)](https://livebook.dev/run?url=https%3A%2F%2Fgithub.com%2Fmishka-group%2Fmishka_developer_tools%2Fblob%2Fmaster%2Fguidance%2Fguarded-struct.livemd) 52 | 53 | --- 54 | 55 | # Donate 56 | 57 | You can support this project through the "[Sponsor](https://github.com/sponsors/mishka-group)" button on GitHub or via cryptocurrency donations. All our projects are **open-source** and **free**, and we rely on community contributions to enhance and improve them further. 58 | 59 | | **BTC** | **ETH** | **DOGE** | **TRX** | 60 | | ---------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | 61 | | | | | | 62 | 63 |
64 | Donate addresses 65 | 66 | **BTC**:‌ 67 | 68 | ``` 69 | bc1q24pmrpn8v9dddgpg3vw9nld6hl9n5dkw5zkf2c 70 | ``` 71 | 72 | **ETH**: 73 | 74 | ``` 75 | 0xD99feB9db83245dE8B9D23052aa8e62feedE764D 76 | ``` 77 | 78 | **DOGE**: 79 | 80 | ``` 81 | DGGT5PfoQsbz3H77sdJ1msfqzfV63Q3nyH 82 | ``` 83 | 84 | **TRX**: 85 | 86 | ``` 87 | TBamHas3wAxSEvtBcWKuT3zphckZo88puz 88 | ``` 89 | 90 |
91 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 0.1.7 | :white_check_mark: | 8 | | < 0.1.7 | :x: | 9 | 10 | ## Reporting a Vulnerability 11 | 12 | Please be sure to contact `info@mishka.life` email if you find a security bug. 13 | -------------------------------------------------------------------------------- /guidance/permission-access.md: -------------------------------------------------------------------------------- 1 | # PermissionAccess 2 | 3 | Consider the scenario in which you are responsible for maintaining each user's access level in the database related to users. In addition, each router in your controller needs to be free for one access while preventing other things from accessing it. 4 | To achieve this goal, the PermissionAccess module provides assistance in implementing a Unix-like mode in the most straightforward manner feasible. 5 | 6 | This module was written with the contribution of Mr. Toomaj Boloorian, 7 | who can be found at the following GitHub address: https://github.com/toomaj 8 | and and Shahryar Tavakkoli: https://github.com/shahryarjb 9 | 10 | --- 11 | 12 | ### Allow access to the user 13 | 14 | `@spec permittes?(user_permissions(), action()) :: boolean` 15 | 16 | Access in this section is referred to as an action, and in addition, there are two sections included. 17 | 18 | The first portion may grant access to an entire department or to an entire role, whereas the second part 19 | may be delegated to a specific part. But this explanation is subject to alter depending on the strategy 20 | you choose. Because of this, pay close attention to the instances that follow. 21 | 22 | Within this section, the user has access to all portions of your program thanks to the wildcard permissions that have been granted to him. 23 | 24 | **Note**: You have the option of assigning a star `*` rating or writing it as `*:*`. 25 | 26 | ```elixir 27 | @admin_router %{ 28 | # admin router 29 | "AdminDashboardLive" => "admin:*", 30 | "AdminBlogPostsLive" => "admin:edit", 31 | "AdminBlogPostLive" => "admin:edit", 32 | "AdminBlogCategoriesLive" => "admin:edit", 33 | "AdminBlogCategoryLive" => "admin:edit", 34 | "AdminBookmarksLive" => "*", 35 | "AdminSubscriptionsLive" => "*", 36 | "AdminSubscriptionLive" => "*", 37 | "AdminCommentsLive" => "admin:edit", 38 | "AdminCommentLive" => "admin:edit", 39 | "AdminUsersLive" => "*", 40 | "AdminUserLive" => "*", 41 | "AdminLogsLive" => "*", 42 | "AdminSeoLive" => "*", 43 | "AdminBlogPostAuthorsLive" => "admin:edit", 44 | "AdminBlogNotifLive" => "*", 45 | "AdminBlogNotifsLive" => "admin:view" 46 | } 47 | 48 | user_actions = [%{value: "*"}] 49 | PermissionAccess.permittes?(user_actions, @admin_router[item]) 50 | # This should be true, and the user has access. 51 | 52 | user_actions = [%{value: "*:edit"}] 53 | PermissionAccess.permittes?(user_actions, @admin_router["AdminBlogNotifsLive"]) 54 | # This should be false, and the user has no access. 55 | 56 | user_actions = [%{value: "*:edit"}, %{value: "admin:view"}] 57 | PermissionAccess.permittes?(user_actions, @admin_router["AdminBlogNotifsLive"]) 58 | # This should be true, and the user has access. 59 | ``` 60 | 61 | --- 62 | 63 | `@spec is_permitted?([{:action, action()} | {:permission, binary}]) :: boolean` 64 | 65 | You will be provided with a list of accesses by the function that was just discussed. However, at its heart lies the following operation: 66 | 67 | This section is identical to the function known as `permittes?/2`, with the exception that 68 | it examines only a single user access rather than a list of accesses. 69 | 70 | ```elixir 71 | PermissionAccess.is_permitted?(action: "*", @admin_router["AdminBlogNotifsLive"]) 72 | ``` 73 | -------------------------------------------------------------------------------- /lib/application.ex: -------------------------------------------------------------------------------- 1 | defmodule MishkaDeveloperTools.Application do 2 | @moduledoc false 3 | use Application 4 | 5 | @impl true 6 | def start(_type, _args) do 7 | children = [] 8 | opts = [strategy: :one_for_one, name: MishkaDeveloperTools.Supervisor] 9 | Supervisor.start_link(children, opts) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/helper/crypto.ex: -------------------------------------------------------------------------------- 1 | defmodule MishkaDeveloperTools.Helper.Crypto do 2 | @moduledoc """ 3 | In reality, this module serves as a support for other libraries in addition 4 | to Erlang's built-in functions for encryption, hashing, and other topics that are associated 5 | with the language. 6 | 7 | It should be brought to your attention that certain functions necessitate the addition of their 8 | dependencies to the primary project. Consequently, prior to making use of these functionalities, 9 | establish the appropriate dependence within the project in accordance with your requirements. 10 | 11 | These functions are custom or wrappers, Copy from: 12 | 13 | - https://hexdocs.pm/plug_crypto/2.0.0/Plug.Crypto.html 14 | - https://hexdocs.pm/phoenix/Phoenix.Token.html 15 | - https://github.com/dashbitco/nimble_totp/blob/master/lib/nimble_totp.ex 16 | - https://dashbit.co/blog/introducing-nimble-totp 17 | - https://hex.pm/packages/bcrypt_elixir 18 | - https://hex.pm/packages/pbkdf2_elixir 19 | - https://password-hashing.net/ 20 | - https://hex.pm/packages/argon2_elixir 21 | - https://github.com/malach-it/boruta_auth 22 | - https://github.com/joken-elixir/joken 23 | - https://hexdocs.pm/phoenix/mix_phx_gen_auth.html 24 | - https://hexdocs.pm/phoenix/Mix.Tasks.Phx.Gen.Secret.html 25 | """ 26 | alias Postgrex.Extensions.JSON 27 | @type based32_url :: <<_::64, _::_*8>> 28 | 29 | @hash_algs %{ 30 | "RS256" => %{"type" => :asymmetric, "hash_algorithm" => :sha256, "binary_size" => 16}, 31 | "RS384" => %{"type" => :asymmetric, "hash_algorithm" => :sha384, "binary_size" => 24}, 32 | "RS512" => %{"type" => :asymmetric, "hash_algorithm" => :sha512, "binary_size" => 32}, 33 | "HS256" => %{"type" => :symmetric, "hash_algorithm" => :sha256, "binary_size" => 16}, 34 | "HS384" => %{"type" => :symmetric, "hash_algorithm" => :sha384, "binary_size" => 24}, 35 | "HS512" => %{"type" => :symmetric, "hash_algorithm" => :sha512, "binary_size" => 32} 36 | } 37 | 38 | @hash_algs_keys Map.keys(@hash_algs) 39 | 40 | @doc """ 41 | Generate a binary composed of random bytes. 42 | 43 | The number of bytes is defined by the `size` argument. Default is `20` per the 44 | [HOTP RFC](https://tools.ietf.org/html/rfc4226#section-4). 45 | 46 | **based on: https://github.com/dashbitco/nimble_totp/blob/master/lib/nimble_totp.ex** 47 | 48 | ## Examples 49 | ```elixir 50 | alias MishkaDeveloperTools.Helper.Crypto 51 | 52 | Crypto.secret() 53 | #=> <<178, 117, 46, 7, 172, 202, 108, 127, 186, 180, ...>> 54 | ``` 55 | """ 56 | @spec secret(integer()) :: any() 57 | def secret(size \\ 20) do 58 | :crypto.strong_rand_bytes(size) 59 | end 60 | 61 | if Code.ensure_loaded?(NimbleTOTP) do 62 | @doc """ 63 | Generates an OTP authentication URL with a given secret, issuer, and label. 64 | 65 | #### Parameters: 66 | 67 | - **secret (binary)**: The secret key used to generate the OTP, 68 | it should be `:crypto.strong_rand_bytes/1`. 69 | - **issuer (String)**: The name of the issuer. 70 | This helps to identify which service the OTP is associated with. 71 | - **label (String)**: A label used to identify the specific account or user. 72 | 73 | #### Returns: 74 | 75 | It returns a map containing: 76 | 77 | - `:secret` (string) - It should be `:crypto.strong_rand_bytes/1` output which is 78 | converted to `base32`. 79 | - `:url` (String) - The complete OTP authentication URL. 80 | 81 | #### Example: 82 | ```elixir 83 | secret = :crypto.strong_rand_bytes(20) 84 | create_otp_link(secret, "Mishka", "user_test") 85 | ``` 86 | 87 | > #### Use cases information {: .info} 88 | > 89 | > You are able to generate a QR code from the URL that you have made and then provide 90 | > it to the user. The majority of devices are able to scan it and utilize it as a 91 | > two-factor authentication one-time password with ease. 92 | 93 | - **More info: https://dashbit.co/blog/introducing-nimble-totp** 94 | """ 95 | @spec create_otp_link(binary(), String.t(), String.t()) :: %{ 96 | secret: String.t(), 97 | url: based32_url() 98 | } 99 | def create_otp_link(secret, issuer, label) do 100 | user_url = NimbleTOTP.otpauth_uri("#{issuer}:#{label}", secret, issuer: issuer) 101 | base32_secret = URI.decode_query(URI.parse(user_url).query)["secret"] 102 | %{secret: base32_secret, url: user_url} 103 | end 104 | 105 | @doc """ 106 | Generates an OTP authentication URL with a given issuer, and label. 107 | 108 | #### Parameters: 109 | 110 | - **issuer (String)**: The name of the issuer. 111 | This helps to identify which service the OTP is associated with. 112 | - **label (String)**: A label used to identify the specific account or user. 113 | 114 | #### Returns: 115 | 116 | It returns a map containing: 117 | 118 | - `:secret` (string) - It should be `:crypto.strong_rand_bytes/1` output which is 119 | converted to `base32`. 120 | - `:url` (String) - The complete OTP authentication URL. 121 | 122 | #### Example: 123 | ```elixir 124 | create_otp_link_and_secret("Mishka", "user_test") 125 | ``` 126 | 127 | > #### Use cases information {: .info} 128 | > 129 | > You are able to generate a QR code from the URL that you have made and then provide 130 | > it to the user. The majority of devices are able to scan it and utilize it as a 131 | > two-factor authentication one-time password with ease. 132 | 133 | - **More info: https://dashbit.co/blog/introducing-nimble-totp** 134 | """ 135 | @spec create_otp_link_and_secret(String.t(), String.t()) :: %{ 136 | secret: String.t(), 137 | url: based32_url() 138 | } 139 | def create_otp_link_and_secret(issuer, label) do 140 | secret = NimbleTOTP.secret() 141 | user_url = NimbleTOTP.otpauth_uri("#{issuer}:#{label}", secret, issuer: issuer) 142 | base32_secret = URI.decode_query(URI.parse(user_url).query)["secret"] 143 | %{secret: base32_secret, url: user_url} 144 | end 145 | 146 | @doc """ 147 | For information See `create_otp_link/3` 148 | """ 149 | @spec create_otp(binary(), String.t(), String.t()) :: %{ 150 | secret: String.t(), 151 | url: based32_url() 152 | } 153 | def create_otp(secret, issuer, label), do: create_otp_link(secret, issuer, label) 154 | 155 | @doc """ 156 | For information See `create_otp_link_and_secret/2` 157 | """ 158 | @spec create_otp(binary(), String.t()) :: %{secret: String.t(), url: <<_::64, _::_*8>>} 159 | def create_otp(issuer, label), do: create_otp_link_and_secret(issuer, label) 160 | 161 | @doc """ 162 | Checks if a provided OTP is valid for the given secret. 163 | 164 | #### Parameters: 165 | 166 | - **secret (binary)**: The secret key used to generate the OTP, 167 | it should be `:crypto.strong_rand_bytes/1`. 168 | - `otp` (String): The OTP to verify, for example `"581234"`. 169 | 170 | #### Example: 171 | ```elixir 172 | valid_otp?(secret, "581234") 173 | 174 | # Or, you might put the time when the token was used for the last time to 175 | # prohibit the user from using it again. 176 | 177 | last_used = System.os_time(:second) 178 | valid_otp?(secret, "581234", last_used) 179 | ``` 180 | 181 | > #### Use cases information {: .tip} 182 | > 183 | > One thing to keep in mind is that you ought to have already kept the 184 | > secret of each user in a location such as a database. 185 | """ 186 | @spec valid_otp?(binary(), String.t()) :: boolean() 187 | def valid_otp?(secret, otp) do 188 | NimbleTOTP.valid?(secret, otp) 189 | end 190 | 191 | @doc """ 192 | Checks if a provided OTP is valid for the given secret which is based32. 193 | 194 | #### Parameters: 195 | 196 | - **secret (binary)**: The secret key used to generate the OTP, 197 | it should be `:crypto.strong_rand_bytes/1`. 198 | - `otp` (String): The OTP to verify, for example `"581234"`. 199 | 200 | #### Example: 201 | ```elixir 202 | valid_otp?(secret, "581234", :base32) 203 | 204 | # Or, you might put the time when the token was used for the last time to 205 | # prohibit the user from using it again. 206 | 207 | last_used = System.os_time(:second) 208 | valid_otp?(secret, "581234", last_used, :base32) 209 | ``` 210 | 211 | > #### Use cases information {: .tip} 212 | > 213 | > One thing to keep in mind is that you ought to have already kept the 214 | > secret of each user in a location such as a database. 215 | """ 216 | @spec valid_otp?(binary(), String.t(), :base32 | NimbleTOTP.time()) :: boolean() 217 | def valid_otp?(secret, otp, :base32), do: base32_valid_otp?(secret, otp) 218 | 219 | def valid_otp?(secret, otp, last_used) do 220 | NimbleTOTP.valid?(secret, otp, since: last_used) 221 | end 222 | 223 | @doc """ 224 | For information See `valid_otp?/3` and `valid_otp?/2` 225 | """ 226 | @spec valid_otp?(binary(), String.t(), NimbleTOTP.time(), :base32) :: boolean() 227 | def valid_otp?(secret, otp, last_used, :base32), do: base32_valid_otp?(secret, otp, last_used) 228 | 229 | @doc """ 230 | For information See `valid_otp?/3` and `valid_otp?/2` 231 | """ 232 | @spec base32_valid_otp?(binary(), String.t()) :: boolean() 233 | def base32_valid_otp?(secret, otp) do 234 | Base.decode32!(secret) 235 | |> NimbleTOTP.valid?(otp) 236 | end 237 | 238 | @doc """ 239 | For information See `valid_otp?/3` and `valid_otp?/2` 240 | """ 241 | @spec base32_valid_otp?(binary(), String.t(), NimbleTOTP.time()) :: boolean() 242 | def base32_valid_otp?(secret, otp, last_used) do 243 | Base.decode32!(secret) 244 | |> NimbleTOTP.valid?(otp, since: last_used) 245 | end 246 | end 247 | 248 | if Code.ensure_loaded?(Bcrypt) or Code.ensure_loaded?(Pbkdf2) or Code.ensure_loaded?(Argon2) do 249 | @doc """ 250 | ### Bcrypt 251 | 252 | - `bcrypt_elixir`: https://hex.pm/packages/bcrypt_elixir 253 | - `LICENSE`: https://github.com/riverrun/comeonin/blob/master/LICENSE 254 | 255 | > #### Use cases information {: .warning} 256 | > 257 | > Make sure you have a `C compiler` installed. See the Comeonin wiki for details. 258 | > Wiki link: https://github.com/riverrun/comeonin/wiki/Requirements 259 | 260 | Bcrypt is a key derivation function for passwords designed by Niels 261 | Provos and David Mazières. Bcrypt is an adaptive function, which means 262 | that it can be configured to remain slow and resistant to brute-force 263 | attacks even as computational power increases. 264 | 265 | Bcrypt has no known vulnerabilities and has been widely tested for over 15 years. 266 | However, as it has a low memory use, it is susceptible to GPU cracking attacks. 267 | 268 | --- 269 | 270 | You are required to make use of this function in order to generate an irreversible (hashed) 271 | duplicate of the user's password when you are storing your password. 272 | 273 | Additionally, you should save it in the database together with other unique features 274 | of your unique program. 275 | 276 | ### Exmple: 277 | 278 | ```elixir 279 | create_hash_password("USER_HARD_PASSWORD", :bcrypt) 280 | ``` 281 | 282 | --- 283 | 284 | ### Pbkdf2 285 | 286 | - `pbkdf2` - pbkdf2_elixir https://hex.pm/packages/pbkdf2_elixir 287 | - `LICENSE`: https://github.com/riverrun/pbkdf2_elixir/blob/master/LICENSE.md 288 | 289 | Pbkdf2 is a password-based key derivation function that uses a password, a variable-length 290 | salt and an iteration count and applies a pseudorandom function to these to produce a key. 291 | 292 | Pbkdf2 has no known vulnerabilities and has been widely tested for over 15 years. 293 | However, like Bcrypt, as it has a low memory use, it is susceptible to GPU cracking attacks. 294 | 295 | The original implementation of Pbkdf2 used SHA-1 as the pseudorandom function, 296 | but this version uses HMAC-SHA-512, the default, or HMAC-SHA-256. 297 | 298 | ### Exmple: 299 | 300 | ```elixir 301 | create_hash_password("USER_HARD_PASSWORD", :pbkdf2) 302 | ``` 303 | 304 | --- 305 | 306 | ### Argon2 307 | 308 | Argon2 is the winner of the Password Hashing Competition (PHC). 309 | 310 | - https://password-hashing.net/ 311 | - `argon2`: argon2_elixir https://hex.pm/packages/argon2_elixir (recommended) 312 | - https://github.com/riverrun/argon2_elixir/blob/master/LICENSE.md 313 | 314 | Argon2 is a memory-hard password hashing function which can be used to hash passwords for credential 315 | storage, key derivation, or other applications. 316 | 317 | Being memory-hard means that it is not only computationally expensive, but it also uses a 318 | lot of memory (which can be configured). This means that it is much more difficult 319 | to attack Argon2 hashes using GPUs or dedicated hardware. 320 | 321 | > #### Use cases information {: .warning} 322 | > 323 | > Make sure you have a `C compiler` installed. See the Comeonin wiki for details. 324 | > Wiki link: https://github.com/riverrun/comeonin/wiki/Requirements 325 | 326 | #### Configuration 327 | The following four parameters can be set in the config file (these can all be overridden using keyword options): 328 | 329 | - t_cost - time cost 330 | > the amount of computation, given in number of iterations 331 | > 332 | > 3 is the default 333 | 334 | - m_cost - memory usage 335 | 336 | > 16 is the default - this will produce a memory usage of 64 MiB (2 ^ 16 KiB) 337 | > 338 | > parallelism - number of parallel threads 339 | > 340 | > 4 is the default 341 | 342 | - argon2_type - argon2 variant to use 343 | 344 | > 0 (Argon2d), 1 (Argon2i) or 2 (Argon2id) 345 | > 346 | > 2 is the default (Argon2id) 347 | 348 | --- 349 | 350 | For verifing you can use like this: 351 | 352 | ```elixir 353 | verify_password(hash, "USER_HARD_PASSWORD", :bcrypt) 354 | 355 | verify_password(hash, "USER_HARD_PASSWORD", :pbkdf2) 356 | 357 | verify_password(hash, "USER_HARD_PASSWORD", :argon2) 358 | ``` 359 | """ 360 | end 361 | 362 | if Code.ensure_loaded?(Bcrypt) do 363 | @spec create_hash_password(String.t(), :argon2 | :bcrypt | :pbkdf2) :: String.t() 364 | def create_hash_password(password, :bcrypt) do 365 | Bcrypt.hash_pwd_salt(password) 366 | end 367 | end 368 | 369 | if Code.ensure_loaded?(Pbkdf2) do 370 | def create_hash_password(password, :pbkdf2) do 371 | Pbkdf2.hash_pwd_salt(password, digest: :sha512) 372 | end 373 | end 374 | 375 | if Code.ensure_loaded?(Argon2) do 376 | def create_hash_password(password, :argon2) do 377 | Argon2.hash_pwd_salt(password, digest: :sha512) 378 | end 379 | end 380 | 381 | if Code.ensure_loaded?(Bcrypt) do 382 | @doc """ 383 | For information See `create_hash_password/2`. 384 | """ 385 | @spec verify_password(binary(), String.t(), :argon2 | :bcrypt | :bcrypt_2b | :pbkdf2) :: 386 | boolean() 387 | def verify_password(hash, password, :bcrypt) do 388 | Bcrypt.verify_pass(password, hash) 389 | end 390 | 391 | def verify_password(hash, password, :bcrypt_2b) do 392 | String.replace_prefix(hash, "$2y$", "$2b$") 393 | |> then(&Bcrypt.verify_pass(password, &1)) 394 | end 395 | end 396 | 397 | if Code.ensure_loaded?(Pbkdf2) do 398 | def verify_password(hash, password, :pbkdf2) do 399 | Pbkdf2.verify_pass(password, hash) 400 | end 401 | end 402 | 403 | if Code.ensure_loaded?(Argon2) do 404 | def verify_password(hash, password, :argon2) do 405 | Argon2.verify_pass(password, hash) 406 | end 407 | end 408 | 409 | if Code.ensure_loaded?(Bcrypt) do 410 | @spec no_user_verify_password(String.t(), :argon2 | :bcrypt | :pbkdf2) :: false 411 | def no_user_verify_password(password, :bcrypt) do 412 | Bcrypt.no_user_verify(password: password) 413 | end 414 | end 415 | 416 | if Code.ensure_loaded?(Pbkdf2) do 417 | def no_user_verify_password(password, :pbkdf2) do 418 | Pbkdf2.no_user_verify(password: password) 419 | end 420 | end 421 | 422 | if Code.ensure_loaded?(Argon2) do 423 | def no_user_verify_password(password, :argon2) do 424 | Argon2.no_user_verify(password: password) 425 | end 426 | end 427 | 428 | @doc """ 429 | This is a straightforward data hashing function that does not differentiate between 430 | **`symmetric`** and **`asymmetric`** functions according to their characteristics. Take, for instance, 431 | the use of **`checksums`** or codes associated with `nonce`, `c_hash`, `at_hash`, 432 | `short-lived Access Token`, and other similar concepts. 433 | 434 | > #### Security issue {: .warning} 435 | > 436 | > It is not recommended to use this function for hashing passwords or JWTs. 437 | 438 | ##### I inspired the initial code from this path: 439 | 440 | - https://github.com/malach-it/boruta_auth/blob/master/lib/boruta/oauth/schemas/client.ex#L173 441 | 442 | ### Example: 443 | ```elixir 444 | simple_hash("Your text", "RS512") 445 | simple_hash("Your text", "RS512", 32) 446 | 447 | # OR 448 | simple_hash() 449 | simple_hash(32) 450 | ``` 451 | 452 | > This function in all types of input and output is as follows 453 | 454 | ```elixir 455 | {URL Encode64, Binary} 456 | ``` 457 | """ 458 | @spec simple_hash(String.t(), String.t()) :: {binary(), binary()} 459 | def simple_hash(text, alg, truncated \\ nil) when alg in @hash_algs_keys do 460 | hashed = 461 | :crypto.hash(@hash_algs[alg]["hash_algorithm"], text) 462 | |> binary_part(0, truncated || @hash_algs[alg]["binary_size"]) 463 | 464 | {Base.url_encode64(hashed, padding: false), hashed} 465 | end 466 | 467 | @doc """ 468 | For information See `simple_hash/2` and `simple_hash/3`. 469 | """ 470 | @spec simple_hash() :: {binary(), binary()} 471 | def simple_hash(rand_size \\ 32) do 472 | token = :crypto.strong_rand_bytes(rand_size) 473 | hashed = :crypto.hash(:sha256, token) 474 | 475 | {Base.url_encode64(token, padding: false), hashed} 476 | end 477 | 478 | if Code.ensure_loaded?(JSON) and Code.ensure_loaded?(Joken) do 479 | defmodule Token do 480 | use Joken.Config 481 | end 482 | 483 | @doc """ 484 | For your convenience, this function will generate a signature for you, allowing you to sign 485 | the data that you desire. It is necessary to create a signature before you can create a `JWT`. 486 | Pay a visit to the `Joken` library if you have certain requirements that you need to fulfill. 487 | 488 | ### Example: 489 | ```elixir 490 | create_signer("HS384", "YOUR SECURE KEY") 491 | create_signer("RS512", %{"pem" => pem_file}) 492 | // OR 493 | create_typed_signer("HS384", "YOUR SECURE KEY") 494 | create_typed_signer("RS512", %{"pem" => pem_file}) 495 | ``` 496 | 497 | For more information about `pem`: 498 | - https://hexdocs.pm/joken/signers.html#pem-privacy-enhanced-mail 499 | 500 | 501 | If you want to create `signer` with pem like RSA as an asymmetric, see `create_typed_signer/2` 502 | or `create_typed_signer/3` 503 | 504 | See this issue: 505 | - https://github.com/joken-elixir/joken/issues/420 506 | """ 507 | @spec create_signer(String.t(), binary() | map()) :: Joken.Signer.t() 508 | def create_signer(alg, key) when alg in @hash_algs_keys do 509 | Joken.Signer.create(alg, key) 510 | end 511 | 512 | @doc """ 513 | For information See `create_signer/2`. 514 | """ 515 | @spec create_typed_signer(String.t(), binary(), binary() | nil) :: Joken.Signer.t() 516 | def create_typed_signer(alg, key, pem \\ nil) when alg in @hash_algs_keys do 517 | case @hash_algs[alg]["type"] do 518 | :asymmetric when not is_nil(pem) -> Joken.Signer.create(alg, %{"pem" => pem}) 519 | _ -> create_signer(alg, key) 520 | end 521 | end 522 | 523 | @doc """ 524 | It is possible to use a signature that you have already made in order to sign a 525 | piece of data by utilizing this function. Take note that signing guarantees 526 | that no changes will be made to the data, and that all of your information will 527 | be entirely transparent. 528 | 529 | ### Example: 530 | ```elixir 531 | signer = create_typed_signer("HS384", "YOUR SECURE KEY") 532 | generate_and_sign(extra_claims, signer) 533 | ``` 534 | 535 | > If you do not send the signer, it will attempt to retrieve it from the config of 536 | > your `Joken` module. 537 | > 538 | > Generates claims with the given token configuration and merges them with the given extra claims. 539 | """ 540 | @spec generate_and_sign(map(), Joken.Signer.t() | nil) :: 541 | {:error, atom() | keyword()} | {:ok, binary(), %{optional(binary()) => any()}} 542 | def generate_and_sign(extra_claims, signer \\ nil) do 543 | if !is_nil(signer), 544 | do: Token.generate_and_sign(extra_claims, signer), 545 | else: Token.generate_and_sign(extra_claims) 546 | end 547 | 548 | @doc """ 549 | For information See `generate_and_sign/2` or `generate_and_sign/1`. 550 | """ 551 | @spec generate_and_sign!(map(), Joken.Signer.t() | nil) :: binary() 552 | def generate_and_sign!(extra_claims, signer \\ nil) do 553 | if !is_nil(signer), 554 | do: Token.generate_and_sign!(extra_claims, signer), 555 | else: Token.generate_and_sign!(extra_claims) 556 | end 557 | 558 | @doc """ 559 | It is like `generate_and_sign/2` or `generate_and_sign/1`, but it did not generate claims. 560 | """ 561 | @spec encode_and_sign(map(), Joken.Signer.t() | nil) :: 562 | {:error, atom() | keyword()} | {:ok, binary(), %{optional(binary()) => any()}} 563 | def encode_and_sign(extra_claims, signer \\ nil) do 564 | if !is_nil(signer), 565 | do: Token.encode_and_sign(extra_claims, signer), 566 | else: Token.encode_and_sign(extra_claims) 567 | end 568 | 569 | @doc """ 570 | Verifies a bearer_token using the given signer and executes hooks if any are given. 571 | 572 | > If you do not send the signer, it will attempt to retrieve it from the config of 573 | > your `Joken` module. 574 | 575 | ### Example: 576 | ```elixir 577 | signer = create_typed_signer("HS384", "YOUR SECURE KEY") 578 | verify_and_validate(token, signer) 579 | ``` 580 | """ 581 | @spec verify_and_validate(binary(), Joken.Signer.t() | nil) :: 582 | {:error, atom() | keyword()} | {:ok, %{optional(binary()) => any()}} 583 | def verify_and_validate(token, signer \\ nil) do 584 | if !is_nil(signer), 585 | do: Token.verify_and_validate(token, signer), 586 | else: Token.verify_and_validate(token) 587 | end 588 | 589 | @doc """ 590 | For information See `verify_and_validate/2` or `verify_and_validate/1`. 591 | """ 592 | @spec verify_and_validate!(binary(), Joken.Signer.t() | nil) :: %{optional(binary()) => any()} 593 | def verify_and_validate!(token, signer \\ nil) do 594 | if !is_nil(signer), 595 | do: Token.verify_and_validate!(token, signer), 596 | else: Token.verify_and_validate!(token) 597 | end 598 | end 599 | 600 | if Code.ensure_loaded?(Plug) do 601 | @doc """ 602 | Encodes, encrypts, and signs data into a token you can send to 603 | clients. Its usage is identical to that of `sign/4`, but the data 604 | is extracted using `decrypt/4`, rather than `verify/4`. 605 | 606 | - Copy from: https://github.com/phoenixframework/phoenix/blob/v1.7.12/lib/phoenix/token.ex 607 | 608 | ## Options 609 | 610 | * `:key_iterations` - option passed to `Plug.Crypto.KeyGenerator` 611 | when generating the encryption and signing keys. Defaults to 1000 612 | * `:key_length` - option passed to `Plug.Crypto.KeyGenerator` 613 | when generating the encryption and signing keys. Defaults to 32 614 | * `:key_digest` - option passed to `Plug.Crypto.KeyGenerator` 615 | when generating the encryption and signing keys. Defaults to `:sha256` 616 | * `:signed_at` - set the timestamp of the token in seconds. 617 | Defaults to `System.system_time(:second)` 618 | * `:max_age` - the default maximum age of the token. Defaults to 619 | 86400 seconds (1 day) and it may be overridden on `decrypt/4`. 620 | """ 621 | def encrypt(key_base, secret, data, opts \\ []) when is_binary(secret) do 622 | key_base 623 | |> Plug.Crypto.encrypt(secret, data, opts) 624 | end 625 | 626 | @doc """ 627 | Decrypts the original data from the token and verifies its integrity. 628 | Its usage is identical to `verify/4` but for encrypted tokens. 629 | 630 | - Copy from: https://github.com/phoenixframework/phoenix/blob/v1.7.12/lib/phoenix/token.ex 631 | 632 | ## Options 633 | 634 | * `:key_iterations` - option passed to `Plug.Crypto.KeyGenerator` 635 | when generating the encryption and signing keys. Defaults to 1000 636 | * `:key_length` - option passed to `Plug.Crypto.KeyGenerator` 637 | when generating the encryption and signing keys. Defaults to 32 638 | * `:key_digest` - option passed to `Plug.Crypto.KeyGenerator` 639 | when generating the encryption and signing keys. Defaults to `:sha256` 640 | * `:max_age` - verifies the token only if it has been generated 641 | "max age" ago in seconds. Defaults to the max age signed in the 642 | token by `encrypt/4`. 643 | """ 644 | def decrypt(key_base, secret, token, opts \\ []) when is_binary(secret) do 645 | key_base 646 | |> Plug.Crypto.decrypt(secret, token, opts) 647 | end 648 | 649 | @doc """ 650 | Decodes the original data from the token and verifies its integrity. 651 | 652 | ## Examples 653 | 654 | In this scenario we will create a token, sign it, then provide it to a client 655 | application. 656 | 657 | ```elixir 658 | iex> user_id = 99 659 | iex> secret = "kjoy3o1zeidquwy1398juxzldjlksahdk3" 660 | iex> namespace = "user auth" 661 | iex> token = sign(secret, namespace, user_id) 662 | ``` 663 | 664 | The mechanism for passing the token to the client is typically through a 665 | cookie, a JSON response body, or HTTP header. For now, assume the client has 666 | received a token it can use to validate requests for protected resources. 667 | 668 | When the server receives a request, it can use `verify/4` to determine if it 669 | should provide the requested resources to the client: 670 | 671 | ```elixir 672 | iex> verify(secret, namespace, token, max_age: 86400) 673 | {:ok, 99} 674 | ``` 675 | 676 | In this example, we know the client sent a valid token because `verify/4` 677 | returned a tuple of type `{:ok, user_id}`. The server can now proceed with 678 | the request. 679 | 680 | However, if the client had sent an expired token, an invalid token, or `nil`, 681 | `verify/4` would have returned an error instead: 682 | 683 | ```elixir 684 | iex> verify(secret, namespace, expired, max_age: 86400) 685 | {:error, :expired} 686 | 687 | iex> verify(secret, namespace, invalid, max_age: 86400) 688 | {:error, :invalid} 689 | 690 | iex> verify(secret, namespace, nil, max_age: 86400) 691 | {:error, :missing} 692 | ``` 693 | 694 | ## Options 695 | 696 | * `:key_iterations` - option passed to `Plug.Crypto.KeyGenerator` 697 | when generating the encryption and signing keys. Defaults to 1000 698 | * `:key_length` - option passed to `Plug.Crypto.KeyGenerator` 699 | when generating the encryption and signing keys. Defaults to 32 700 | * `:key_digest` - option passed to `Plug.Crypto.KeyGenerator` 701 | when generating the encryption and signing keys. Defaults to `:sha256` 702 | * `:max_age` - verifies the token only if it has been generated 703 | "max age" ago in seconds. Defaults to the max age signed in the 704 | token by `sign/4`. 705 | """ 706 | def verify(key_base, salt, token, opts \\ []) when is_binary(salt) do 707 | key_base 708 | |> Plug.Crypto.verify(salt, token, opts) 709 | end 710 | 711 | @doc """ 712 | Encodes and signs data into a token you can send to clients. 713 | 714 | ## Options 715 | 716 | * `:key_iterations` - option passed to `Plug.Crypto.KeyGenerator` 717 | when generating the encryption and signing keys. Defaults to 1000 718 | * `:key_length` - option passed to `Plug.Crypto.KeyGenerator` 719 | when generating the encryption and signing keys. Defaults to 32 720 | * `:key_digest` - option passed to `Plug.Crypto.KeyGenerator` 721 | when generating the encryption and signing keys. Defaults to `:sha256` 722 | * `:signed_at` - set the timestamp of the token in seconds. 723 | Defaults to `System.system_time(:second)` 724 | * `:max_age` - the default maximum age of the token. Defaults to 725 | 86400 seconds (1 day) and it may be overridden on `verify/4`. 726 | 727 | ### Example: 728 | ```elixir 729 | key_base = random_key_base() 730 | sign(key_base, "user-secret", {:elixir, :terms}) 731 | ``` 732 | """ 733 | def sign(key_base, salt, data, opts \\ []) when is_binary(salt) do 734 | key_base 735 | |> Plug.Crypto.sign(salt, data, opts) 736 | end 737 | end 738 | 739 | @doc false 740 | def random_key_base(length \\ 64) when length > 31 do 741 | :crypto.strong_rand_bytes(length) 742 | |> Base.encode64(padding: false) 743 | |> binary_part(0, length) 744 | end 745 | end 746 | -------------------------------------------------------------------------------- /lib/helper/extra.ex: -------------------------------------------------------------------------------- 1 | defmodule MishkaDeveloperTools.Helper.Extra do 2 | @alphabet Enum.concat([?0..?9, ?A..?Z, ?a..?z]) 3 | @username_start ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "_"] 4 | @username_alphabet Enum.concat([?0..?9, ?A..?Z, ?a..?z, ~c"_"]) 5 | 6 | # Do not use for security and sensitive cases 7 | @spec randstring(integer) :: binary 8 | def randstring(count) do 9 | :rand.seed(:exsplus, :os.timestamp()) 10 | 11 | Stream.repeatedly(&random_char_from_alphabet/0) 12 | |> Enum.take(count) 13 | |> List.to_string() 14 | |> String.upcase() 15 | end 16 | 17 | defp random_char_from_alphabet() do 18 | Enum.random(@alphabet) 19 | end 20 | 21 | def app_started?(name) do 22 | Application.started_applications() 23 | |> Enum.map(fn {app, _, _} -> app end) 24 | |> Enum.member?(name) 25 | end 26 | 27 | # based on: https://www.erlang.org/doc/apps/erts/match_spec 28 | def erlang_guard(:or), do: :orelse 29 | def erlang_guard(:and), do: :andalso 30 | def erlang_guard(:xor), do: :xor 31 | def erlang_guard(:<=), do: :"=<" 32 | def erlang_guard(:!=), do: :"/=" 33 | def erlang_guard(:===), do: :"=:=" 34 | def erlang_guard(:!==), do: :"=/=" 35 | def erlang_guard(term), do: term 36 | 37 | def erlang_result(:all), do: [:"$_"] 38 | def erlang_result(:selected), do: [:"$$"] 39 | def erlang_result(term), do: term 40 | 41 | def erlang_fields(tuple, [], _keys, _num), do: tuple 42 | 43 | def erlang_fields(tuple, [h | t], keys, num) do 44 | new_tuple = 45 | Tuple.insert_at( 46 | tuple, 47 | tuple_size(tuple), 48 | if(h in keys, do: String.to_atom("$#{num}"), else: :_) 49 | ) 50 | 51 | erlang_fields(new_tuple, t, keys, if(h in keys, do: num + 1, else: num)) 52 | end 53 | 54 | def timestamp() do 55 | DateTime.utc_now() 56 | |> DateTime.truncate(:microsecond) 57 | end 58 | 59 | def validated_user?(username) when is_binary(username) do 60 | username_regex = ~r/^[a-zA-Z][a-zA-Z0-9_]{4,34}$/ 61 | username_length = String.length(username) 62 | start_status = String.starts_with?(username, @username_start) 63 | characters? = check_specific_characters?(username, @username_alphabet) 64 | 65 | if username_length > 4 and username_length < 35 and List.ascii_printable?(~c"#{username}") and 66 | !start_status and characters? do 67 | username 68 | |> String.trim() 69 | |> then(&Regex.match?(username_regex, &1)) 70 | else 71 | false 72 | end 73 | end 74 | 75 | def validated_user?(_username), do: false 76 | 77 | def validated_password?(password) when is_binary(password) do 78 | regex_pattern = ~r/^[0-9A-Za-z@#$%^_\-!(,)*]{9,130}$/ 79 | 80 | Enum.all?([ 81 | Regex.match?(regex_pattern, password), 82 | Regex.match?(~r/[A-Z]/, password), 83 | Regex.match?(~r/[a-z]/, password), 84 | Regex.match?(~r/\d/, password), 85 | Regex.match?(~r/[@#$%^_\-!(,)*]/, password) 86 | ]) 87 | end 88 | 89 | def validated_password?(_password), do: false 90 | 91 | defp check_specific_characters?(input, allowed_chars) do 92 | input 93 | |> String.to_charlist() 94 | |> Enum.all?(&(&1 in allowed_chars)) 95 | end 96 | 97 | def get_unix_time() do 98 | DateTime.utc_now() |> DateTime.to_unix() 99 | end 100 | 101 | def get_unix_time_with_shift(number, type \\ :second) 102 | when type in [:day, :hour, :minute, :second] do 103 | DateTime.utc_now() 104 | |> DateTime.add(number, type) 105 | |> DateTime.to_unix() 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /lib/helper/uuid.ex: -------------------------------------------------------------------------------- 1 | defmodule MishkaDeveloperTools.Helper.UUID do 2 | @moduledoc """ 3 | **To make the UUID, this module is completely copied from the ecto project under the Apache License 2.0.** 4 | **For more information and more functions, please refer to the following path:** 5 | 6 | - https://github.com/elixir-ecto/ecto/blob/v3.11.2/lib/ecto/uuid.ex#L1 7 | - https://hexdocs.pm/ecto/Ecto.UUID.html 8 | 9 | > It should be noted that this file only supports uuid version 4. 10 | > To use other versions in hex, look for another library 11 | """ 12 | 13 | @type t :: <<_::288>> 14 | @type raw :: <<_::128>> 15 | 16 | defmodule HelperEcto.CastError do 17 | @moduledoc """ 18 | Raised when a changeset can't cast a value. 19 | """ 20 | defexception [:message, :type, :value] 21 | 22 | def exception(opts) do 23 | type = Keyword.fetch!(opts, :type) 24 | value = Keyword.fetch!(opts, :value) 25 | msg = opts[:message] || "cannot cast #{inspect(value)} to #{format(type)}" 26 | %__MODULE__{message: msg, type: type, value: value} 27 | end 28 | 29 | @doc """ 30 | Format type for error messaging and logs. 31 | """ 32 | def format({composite, type}) when composite in [:array, :map] do 33 | "{#{inspect(composite)}, #{format(type)}}" 34 | end 35 | 36 | def format({:parameterized, type, params}) do 37 | if function_exported?(type, :format, 1) do 38 | apply(type, :format, [params]) 39 | else 40 | "##{inspect(type)}<#{inspect(params)}>" 41 | end 42 | end 43 | 44 | def format(type), do: inspect(type) 45 | end 46 | 47 | @spec cast(t | raw | any) :: {:ok, t} | :error 48 | def cast(uuid) 49 | 50 | def cast( 51 | <> 53 | ) do 54 | <> 57 | catch 58 | :error -> :error 59 | else 60 | hex_uuid -> {:ok, hex_uuid} 61 | end 62 | 63 | def cast(<<_::128>> = raw_uuid), do: {:ok, encode(raw_uuid)} 64 | def cast(_), do: :error 65 | 66 | @spec cast!(t | raw | any) :: t 67 | def cast!(uuid) do 68 | case cast(uuid) do 69 | {:ok, hex_uuid} -> hex_uuid 70 | :error -> raise HelperEcto.CastError, type: __MODULE__, value: uuid 71 | end 72 | end 73 | 74 | @compile {:inline, c: 1} 75 | 76 | defp c(?0), do: ?0 77 | defp c(?1), do: ?1 78 | defp c(?2), do: ?2 79 | defp c(?3), do: ?3 80 | defp c(?4), do: ?4 81 | defp c(?5), do: ?5 82 | defp c(?6), do: ?6 83 | defp c(?7), do: ?7 84 | defp c(?8), do: ?8 85 | defp c(?9), do: ?9 86 | defp c(?A), do: ?a 87 | defp c(?B), do: ?b 88 | defp c(?C), do: ?c 89 | defp c(?D), do: ?d 90 | defp c(?E), do: ?e 91 | defp c(?F), do: ?f 92 | defp c(?a), do: ?a 93 | defp c(?b), do: ?b 94 | defp c(?c), do: ?c 95 | defp c(?d), do: ?d 96 | defp c(?e), do: ?e 97 | defp c(?f), do: ?f 98 | defp c(_), do: throw(:error) 99 | 100 | @spec generate() :: t 101 | def generate(), do: encode(bingenerate()) 102 | 103 | @spec bingenerate() :: raw 104 | def bingenerate() do 105 | <> = :crypto.strong_rand_bytes(16) 106 | <> 107 | end 108 | 109 | @doc false 110 | def autogenerate, do: generate() 111 | 112 | @spec encode(raw) :: t 113 | defp encode( 114 | <> 117 | ) do 118 | <> 121 | end 122 | 123 | @compile {:inline, e: 1} 124 | 125 | defp e(0), do: ?0 126 | defp e(1), do: ?1 127 | defp e(2), do: ?2 128 | defp e(3), do: ?3 129 | defp e(4), do: ?4 130 | defp e(5), do: ?5 131 | defp e(6), do: ?6 132 | defp e(7), do: ?7 133 | defp e(8), do: ?8 134 | defp e(9), do: ?9 135 | defp e(10), do: ?a 136 | defp e(11), do: ?b 137 | defp e(12), do: ?c 138 | defp e(13), do: ?d 139 | defp e(14), do: ?e 140 | defp e(15), do: ?f 141 | end 142 | -------------------------------------------------------------------------------- /lib/mishka_developer_tools.ex: -------------------------------------------------------------------------------- 1 | defmodule MishkaDeveloperTools do 2 | @moduledoc """ 3 | # Mishka Elixir Developer Tools 4 | 5 | We tried to deliver a series of our client's [**CMS**](https://github.com/mishka-group/mishka-cms) built on [**Elixir**](https://elixir-lang.org/) at the start of the [**Mishka Group**](https://github.com/mishka-group) project, but we recently archived this open-source project and have yet to make plans to rework and expand it. This system was created using [**Phoenix**](https://www.phoenixframework.org/) and [**Phoenix LiveView**](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html). After a long period, a series of macros and functional modules emerged from this project and our other projects, which we are gradually publishing in this library. 6 | 7 | > **NOTICE**: Do not use the master branch; this library is under heavy development. Expect version `0.1.7`, and for using the new features, please wait until a new release is out. 8 | 9 | --- 10 | 11 | - ### [GuardedStruct](https://github.com/mishka-group/mishka_developer_tools/blob/master/guidance/guarded-struct.md) 12 | 13 | > The creation of this macro will allow you to build `Structs` that provide you with a number of important options, including the following: 14 | > 15 | > 1. Validation 16 | > 2. Sanitizing 17 | > 3. Constructor 18 | > 4. It provides the capacity to operate in a nested style simultaneously. 19 | 20 | - ### [PermissionAccess](https://github.com/mishka-group/mishka_developer_tools/blob/master/guidance/permission-access.md) 21 | 22 | > Consider the scenario in which you are responsible for maintaining each user's access level in the database related to users. 23 | > **It is unix like way**. 24 | 25 | --- 26 | 27 | > **Mishka developer tools** provides some macros and modules to make creating your elixir application as easy as possible 28 | 29 | ## Installation 30 | 31 | The package can be installed by adding `mishka_developer_tools` to your list of dependencies in `mix.exs`: 32 | 33 | ```elixir 34 | def deps do 35 | [ 36 | {:mishka_developer_tools, "~> 0.1.7"} 37 | ] 38 | end 39 | ``` 40 | 41 | The docs can be found at [https://hexdocs.pm/mishka_developer_tools](https://hexdocs.pm/mishka_developer_tools). 42 | 43 | [![Run in Livebook](https://livebook.dev/badge/v1/pink.svg)](https://livebook.dev/run?url=https%3A%2F%2Fgithub.com%2Fmishka-group%2Fmishka_developer_tools%2Fblob%2Fmaster%2Fguidance%2Fguarded-struct.livemd) 44 | 45 | --- 46 | 47 | # Donate 48 | 49 | If the project was useful for you, the only way you can donate to me is the following ways 50 | 51 | | **BTC** | **ETH** | **DOGE** | **TRX** | 52 | | ---------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | 53 | | | | | | 54 | 55 |
56 | Donate addresses 57 | 58 | **BTC**:‌ 59 | 60 | ``` 61 | bc1q24pmrpn8v9dddgpg3vw9nld6hl9n5dkw5zkf2c 62 | ``` 63 | 64 | **ETH**: 65 | 66 | ``` 67 | 0xD99feB9db83245dE8B9D23052aa8e62feedE764D 68 | ``` 69 | 70 | **DOGE**: 71 | 72 | ``` 73 | DGGT5PfoQsbz3H77sdJ1msfqzfV63Q3nyH 74 | ``` 75 | 76 | **TRX**: 77 | 78 | ``` 79 | TBamHas3wAxSEvtBcWKuT3zphckZo88puz 80 | ``` 81 | 82 |
83 | 84 | """ 85 | end 86 | -------------------------------------------------------------------------------- /lib/modules/mnesia_assistant/error.ex: -------------------------------------------------------------------------------- 1 | defmodule MnesiaAssistant.Error do 2 | alias :mnesia, as: Mnesia 3 | require Logger 4 | # types link https://www.erlang.org/doc/man/mnesia#data-types 5 | 6 | @error_types [ 7 | :nested_transaction, 8 | :badarg, 9 | :no_transaction, 10 | :combine_error, 11 | :bad_index, 12 | :already_exists, 13 | :index_exists, 14 | :no_exists, 15 | :system_limit, 16 | :mnesia_down, 17 | :not_a_db_node, 18 | :bad_type, 19 | :node_not_running, 20 | :truncated_binary_file, 21 | :active, 22 | :illegal 23 | ] 24 | 25 | @doc """ 26 | ### Erlang document: 27 | 28 | All Mnesia transactions, including all the schema update functions, either return value `{atomic, Val}` 29 | or the tuple `{aborted, Reason}`. Reason can be either of the atoms in the following list. 30 | The function `error_description/1` returns a descriptive string that describes the error. 31 | 32 | 33 | - `nested_transaction`. Nested transactions are not allowed in this context. 34 | - `badarg`. Bad or invalid argument, possibly bad type. 35 | - `no_transaction`. Operation not allowed outside transactions. 36 | - `combine_error`. Table options illegally combined. 37 | - `bad_index`. Index already exists, or was out of bounds. 38 | - `already_exists`. Schema option to be activated is already on. 39 | - `index_exists`. Some operations cannot be performed on tables with an index. 40 | - `no_exists`. Tried to perform operation on non-existing (not-alive) item. 41 | - `system_limit`. A system limit was exhausted. 42 | - `mnesia_down`. A transaction involves records on a remote node, which became unavailable 43 | before the transaction was completed. Records are no longer available elsewhere in the network. 44 | - `not_a_db_node`. A node was mentioned that does not exist in the schema. 45 | - `bad_type`. Bad type specified in argument. 46 | - `node_not_running`. Node is not running. 47 | - `truncated_binary_file`. Truncated binary in file. 48 | - `active`. Some delete operations require that all active records are removed. 49 | - `illegal`. Operation not supported on this record. 50 | 51 | Error can be Reason, `{error, Reason}`, or `{aborted, Reason}`. Reason can be an atom or a 52 | tuple with Reason as an atom in the first field. 53 | 54 | The following examples illustrate a function that returns an error, and the method to 55 | retrieve more detailed error information: 56 | 57 | - The function `mnesia:create_table(bar, [{attributes, 3.14}])` returns the tuple `{aborted,Reason}`, where 58 | Reason is the tuple `{bad_type,bar,3.14000}`. 59 | 60 | The function `mnesia:error_description(Reason)` returns the term `{"Bad type on some provided arguments",bar,3.14000}`, 61 | which is an error description suitable for display. 62 | 63 | - `error_description(Error :: term())` 64 | 65 | ### Example: 66 | 67 | ```elixir 68 | MnesiaAssistant.Error.error_description(error) 69 | ``` 70 | """ 71 | def error_description(error), do: Mnesia.error_description(error) 72 | 73 | def error_description(error, identifier) when error in [{:atomic, :ok}, :ok] do 74 | Logger.info( 75 | "Identifier: #{inspect(identifier)} ::: MnesiaMessage: The action concerned was completed successfully." 76 | ) 77 | 78 | {:ok, :atomic} 79 | end 80 | 81 | def error_description(:starting, identifier) do 82 | Logger.info( 83 | "Identifier: #{inspect(identifier)} ::: MnesiaMessage: The action concerned is being started successfully." 84 | ) 85 | 86 | {:ok, :atomic} 87 | end 88 | 89 | def error_description({:aborted, {:already_exists, _module}} = error, identifier) do 90 | converted = error_description(error) 91 | 92 | Logger.warning( 93 | "Identifier: #{inspect(identifier)} ::: MnesiaError: #{inspect(error)} ::: ConvertedError: #{inspect(converted)}" 94 | ) 95 | 96 | err = if is_tuple(converted), do: to_string(elem(converted, 0)), else: converted 97 | {:error, error, err} 98 | end 99 | 100 | def error_description({:error, {_, {:already_exists, _}}} = error, identifier) do 101 | converted = error_description(error) 102 | 103 | Logger.warning( 104 | "Identifier: #{inspect(identifier)} ::: MnesiaError: #{inspect(error)} ::: ConvertedError: #{inspect(converted)}" 105 | ) 106 | 107 | err = if is_tuple(converted), do: to_string(elem(converted, 0)), else: converted 108 | {:error, error, err} 109 | end 110 | 111 | def error_description({:aborted, error_type} = error, identifier) when is_tuple(error_type) do 112 | if elem(error_type, 0) in @error_types do 113 | converted = error_description(error) 114 | 115 | Logger.error( 116 | "Identifier: #{inspect(identifier)} ::: MnesiaError: #{inspect(error)} ::: ConvertedError: #{inspect(converted)}" 117 | ) 118 | 119 | err = if is_tuple(converted), do: to_string(elem(converted, 0)), else: converted 120 | {:error, error, err} 121 | else 122 | error_description(error, identifier) 123 | end 124 | end 125 | 126 | def error_description(error, identifier) do 127 | converted = error_description(error) 128 | 129 | Logger.error( 130 | "Identifier: #{inspect(identifier)} ::: MnesiaError: #{inspect(error)} ::: ConvertedError: #{inspect(converted)}" 131 | ) 132 | 133 | {:error, error, converted} 134 | end 135 | 136 | def try?({:error, {:timeout, _missing_tables}, _}, max, current) when current <= max, do: true 137 | 138 | def try?({:error, {:timeout, _missing_tables}, _}, max, current) when current > max, do: false 139 | 140 | def try?({:ok, :atomic}, _max, _current), do: false 141 | 142 | def try?(_error, _max, _current), do: false 143 | 144 | def try?({:error, {:timeout, _missing_tables}, _}), do: true 145 | 146 | def try?({:ok, :atomic}), do: false 147 | 148 | def try?(_error), do: false 149 | end 150 | -------------------------------------------------------------------------------- /lib/modules/mnesia_assistant/mnesia_assistant.ex: -------------------------------------------------------------------------------- 1 | defmodule MnesiaAssistant do 2 | @moduledoc """ 3 | `MnesiaAssistant` is a wrapper for the Mnesia (Top level Erlang runtime database, ETS) module. 4 | Its primary purpose is to facilitate the utilisation of this database in Elixir. 5 | Additionally, it offers a number of features, such as the standardisation of 6 | the output and routes, as well as various hooks and helpers in this particular domain. 7 | 8 | The following is a list of the primary modules that you plan on using in your project: 9 | 10 | 1. `MnesiaAssistant.Schema` 11 | 2. `MnesiaAssistant.Table` 12 | 3. `MnesiaAssistant.Query` 13 | 4. `MnesiaAssistant.Transaction` 14 | 5. `MnesiaAssistant.Information` 15 | 6. `MnesiaAssistant.BackupAndRestore` 16 | """ 17 | 18 | require Logger 19 | alias MnesiaAssistant.{Information, Schema} 20 | alias MishkaDeveloperTools.Helper.Extra 21 | alias MnesiaAssistant.Error, as: MError 22 | alias :mnesia, as: Mnesia 23 | 24 | ################################################################ 25 | ######################### Public Apis ########################## 26 | ################################################################ 27 | 28 | ################# Global functions Public Apis ################# 29 | @doc """ 30 | In order to fulfil the requirements of the **Elixir** project, 31 | it is necessary for you to activate this database in 32 | addition to executing the `mnesia` function within the `mix.exs` file. 33 | Both activation and information for determining whether or not it is 34 | active are accomplished through the utilisation of these functions. 35 | 36 | Two distinct variants of this function have been generated for your use. 37 | It is important to note that the `start/0` function is a wrapper 38 | from Erlang, while the second function is responsible for activating 39 | `mnesia` in Elixir (`start/1` :: `:app`). 40 | 41 | > In order to activate programmes, we utilise the `start/1` command with 42 | > the input `:app`. 43 | 44 | ### Example 45 | 46 | ```elixir 47 | MnesiaAssistant.start() # --> :mnesia.start() 48 | # OR 49 | MnesiaAssistant.start(:app) # --> Application.start(:mnesia) 50 | ``` 51 | """ 52 | def start(), do: Mnesia.start() 53 | 54 | @doc """ 55 | Read `start/0` document. 56 | """ 57 | def start(:app), do: Application.start(:mnesia) 58 | 59 | @doc false 60 | def start(mode, dir, identifier) do 61 | Logger.warning( 62 | "Identifier: #{inspect(identifier)} ::: MnesiaMessage: Mnesia's initial valuation process has begun." 63 | ) 64 | 65 | stop() |> MError.error_description() 66 | mnesia_dir = dir <> "/#{mode}" 67 | File.mkdir_p(mnesia_dir) |> MError.error_description(identifier) 68 | Application.put_env(:mnesia, :dir, mnesia_dir |> to_charlist) 69 | MnesiaAssistant.Schema.create_schema([node()]) |> MError.error_description(identifier) 70 | start() |> MError.error_description(identifier) 71 | end 72 | 73 | @doc """ 74 | The `mnesia` database provides you with the capability to perform your storage 75 | either on the hard drive or on the RAM, or both at the same time, depending 76 | on the software approach that you have chosen. In order to accomplish this goal, 77 | its path for storage on the disc (the position of storage) must be known. 78 | This can be accomplished in a number of different ways. 79 | 80 | You can begin the compilation process by using the configuration file of 81 | your programme. In this technique, you will need to specify the directory 82 | for the `:mnesia` (`:dir`) command. And then there is the second technique, 83 | which is the function of the same function and is carried out in the form of run time. 84 | 85 | ### Example 86 | 87 | ```elixir 88 | MnesiaAssistant.set_dir("/tmp/db") # --> Application.put_env(:mnesia, :dir, dir) 89 | ``` 90 | """ 91 | def set_dir(dir), do: Application.put_env(:mnesia, :dir, dir) 92 | 93 | @doc """ 94 | You have the ability to disable mnesia by using the function. 95 | In the same way that the `start/0` function has two forms, 96 | this function also has two forms. One of the forms is a wrapper 97 | for the `mnesia` function itself, while the other form disables 98 | the `mnesia` **Elixir** red applications that have been active. 99 | 100 | ### Example 101 | 102 | ```elixir 103 | MnesiaAssistant.stop() # --> :mnesia.stop() 104 | # OR 105 | MnesiaAssistant.stop(:app) # --> Application.stop(:mnesia) 106 | ``` 107 | """ 108 | def stop(), do: Mnesia.stop() 109 | 110 | @doc """ 111 | Read `stop/1` document. 112 | """ 113 | def stop(:app), do: Application.stop(:mnesia) 114 | 115 | @doc """ 116 | This function determines whether or not the `mnesia` function is active. 117 | The output of the `Application.started_applications()` function 118 | is what this function actually searches for. 119 | 120 | ### Example 121 | 122 | ```elixir 123 | MnesiaAssistant.started?() 124 | ``` 125 | """ 126 | def started?(), do: Extra.app_started?(:mnesia) 127 | 128 | ############### Information functions Public Apis ############### 129 | defdelegate info(), to: Information 130 | 131 | defdelegate info(type), to: Information 132 | 133 | defdelegate set_debug_level(level), to: Information 134 | 135 | defdelegate schema(), to: Schema 136 | 137 | defdelegate schema(table), to: Schema 138 | 139 | defdelegate eg(operation), to: Extra, as: :erlang_guard 140 | 141 | defdelegate er(operation), to: Extra, as: :erlang_result 142 | 143 | defdelegate erl_fields(tuple, fields, keys, num), to: Extra, as: :erlang_fields 144 | ############### Global functions Public Apis ############### 145 | # Ref: https://www.erlang.org/doc/apps/mnesia/mnesia_chap5#mnesia-event-handling 146 | # system | activity | {table, table(), simple | detailed} 147 | @doc """ 148 | Using this function, you will be able to subscribe to Mnesia's activities 149 | and events, and you will receive notifications immediately after an 150 | event takes place. For example, if you are using `GenServer` and you want 151 | to carry out a particular activity in real time based on a strategy, you can 152 | make use of this method. 153 | 154 | ### Example 155 | 156 | ```elixir 157 | MnesiaAssistant.subscribe({:table, Person}) 158 | # -> :mnesia.subscribe({:table, table}) 159 | MnesiaAssistant.subscribe({:table, Person, simple_detailed}) 160 | # -> :mnesia.subscribe({:table, table, simple_detailed}) 161 | MnesiaAssistant.subscribe(what) 162 | # -> :mnesia.subscribe(what) 163 | # What = system | activity | {table, table(), simple | detailed} 164 | ``` 165 | """ 166 | def subscribe({:table, table, simple_detailed}), 167 | do: Mnesia.subscribe({:table, table, simple_detailed}) 168 | 169 | def subscribe(what), do: Mnesia.subscribe(what) 170 | 171 | @doc """ 172 | In order to terminate your subscription to mnesia, you can use the following function. 173 | For more information read `subscribe/1` 174 | 175 | ### Example 176 | 177 | ```elixir 178 | MnesiaAssistant.unsubscribe({:table, Person}) 179 | # -> :mnesia.unsubscribe({:table, table}) 180 | MnesiaAssistant.unsubscribe({:table, Person, simple_detailed}) 181 | # -> :mnesia.unsubscribe({:table, table, simple_detailed}) 182 | MnesiaAssistant.unsubscribe(what) 183 | # -> :mnesia.unsubscribe(what) 184 | # What = system | activity | {table, table(), simple | detailed} 185 | ``` 186 | """ 187 | def unsubscribe({:table, table, simple_detailed}), 188 | do: Mnesia.unsubscribe({:table, table, simple_detailed}) 189 | 190 | def unsubscribe(what), do: Mnesia.unsubscribe(what) 191 | 192 | @doc """ 193 | When tracing a system of Mnesia applications it is useful to be able to 194 | interleave Mnesia own events with `application-related` events that give 195 | information about the application context. 196 | 197 | Whenever the application begins a new and demanding Mnesia task, 198 | or if it enters a new interesting phase in its execution, it can be a good idea 199 | to use mnesia:report_event/1. Event can be any term and generates 200 | a `{mnesia_user, Event}` event for any processes that subscribe 201 | to Mnesia system events. for more information read `subscribe/1` document. 202 | 203 | ### Example: 204 | 205 | ```elixir 206 | MnesiaAssistant.report_event(event) 207 | ``` 208 | """ 209 | def report_event(event), do: Mnesia.report_event(event) 210 | 211 | @doc """ 212 | The `:mnesia.change_config/2` function in Mnesia, the distributed database management 213 | system in Erlang/OTP, is used to dynamically change the configuration parameters 214 | of the Mnesia system while it is running. 215 | 216 | This function allows you to adjust certain operational parameters of Mnesia 217 | without needing to stop and restart the database, 218 | making it particularly useful for tuning performance or behavior in live systems. 219 | 220 | * `config`: --> extra_db_nodes | dc_dump_limit 221 | * `value`: --> [node()] | number() 222 | 223 | ### Example 224 | 225 | ```elixir 226 | MnesiaAssistant.change_config(config, value) 227 | ``` 228 | """ 229 | def change_config(config, value) when config in [:extra_db_nodes, :dc_dump_limit], 230 | do: Mnesia.change_config(config, value) 231 | 232 | ################# Global Helper Public Apis ################# 233 | def tuple_to_map(:"$end_of_table", _fields, _drop), do: [] 234 | 235 | def tuple_to_map({records, cont}, fields, drop), do: {tuple_to_map(records, fields, drop), cont} 236 | 237 | def tuple_to_map(records, fields, drop) do 238 | Enum.reduce(records, [], fn item, acc -> 239 | converted = 240 | Enum.zip(fields, Tuple.delete_at(item, 0) |> Tuple.to_list()) 241 | |> Enum.into(%{}) 242 | |> Map.drop(drop) 243 | 244 | acc ++ [converted] 245 | end) 246 | end 247 | 248 | def tuple_to_map(:"$end_of_table", _fields, _, _drop), do: [] 249 | 250 | def tuple_to_map(records, fields, nil, drop), do: tuple_to_map(records, fields, drop) 251 | 252 | def tuple_to_map({records, cont}, fields, strc, drop) do 253 | {tuple_to_map(records, fields, strc, drop), cont} 254 | end 255 | 256 | def tuple_to_map(records, fields, strc, drop) do 257 | Enum.reduce(records, [], fn item, acc -> 258 | converted = 259 | Enum.zip(fields, Tuple.delete_at(item, 0) |> Tuple.to_list()) 260 | |> Enum.into(%{}) 261 | |> Map.drop(drop) 262 | 263 | acc ++ [struct!(strc, converted)] 264 | end) 265 | end 266 | 267 | ################# Global Macro Public Apis ################# 268 | # Based on https://github.com/mishka-group/mishka_developer_tools/issues/28 269 | # TODO: we need some strategy to implement fragment 270 | # {:frag_properties, 271 | # [ 272 | # {:node_pool, all_active_nodes}, 273 | # {:n_fragments, 4}, 274 | # {:n_disc_only_copies, all_active_nodes |> length} 275 | # ]} 276 | end 277 | -------------------------------------------------------------------------------- /lib/modules/mnesia_assistant/operation/backup_and_restore.ex: -------------------------------------------------------------------------------- 1 | defmodule MnesiaAssistant.BackupAndRestore do 2 | @moduledoc """ 3 | This module is where you will find the collection of functions that can 4 | assist you in obtaining a support version of `Mnesia` or in recovering the 5 | backup that was stored in the system. 6 | Additionally, it is possible to have a variety of inputs and consequently outputs. 7 | """ 8 | alias :mnesia, as: Mnesia 9 | 10 | @doc """ 11 | The `:mnesia.load_textfile/1` function in `Mnesia` is used to load data from a 12 | text file into a Mnesia table. The text file should contain Erlang terms, 13 | with one term per line, matching the structure of the records in the target table. 14 | 15 | ### Example 16 | 17 | ```elixir 18 | MnesiaAssistant.BackupAndRestore.load_textfile("/path/to/person.txt") 19 | ``` 20 | """ 21 | def load_textfile(path), do: Mnesia.load_textfile(path) 22 | 23 | @doc """ 24 | The `:mnesia.dump_to_textfile/1` function in `Mnesia` is used to dump the contents 25 | of one or more Mnesia tables to a text file. 26 | Each record is written to the file as an Erlang term, one term per line. 27 | This can be useful for backing up data or exporting it for 28 | analysis or use in other systems. 29 | 30 | > Dumps all local tables of a Mnesia system into a text file, 31 | > which can be edited (by a normal text editor) and then be reloaded 32 | > with `mnesia:load_textfile/1`. 33 | > Only use this function for educational purposes. 34 | > Use other functions to deal with real backups. 35 | 36 | ### Example 37 | 38 | ```elixir 39 | MnesiaAssistant.BackupAndRestore.dump_to_textfile("/path/to/person.txt") 40 | ``` 41 | """ 42 | def dump_to_textfile(path), do: Mnesia.dump_to_textfile(path) 43 | 44 | @doc """ 45 | Sets master nodes for `mnesia`. 46 | 47 | > For each table Mnesia determines its replica nodes (TabNodes) and 48 | > starts `mnesia:set_master_nodes(Tab, TabMasterNodes)`. 49 | > where `TabMasterNodes` is the intersection of `MasterNodes` and `TabNodes`. 50 | > For semantics, see `mnesia:set_master_nodes/2`. 51 | 52 | ### Example 53 | 54 | ```elixir 55 | MnesiaAssistant.BackupAndRestore.set_master_nodes([node()]) 56 | ``` 57 | """ 58 | def set_master_nodes(nodes) when is_list(nodes), do: Mnesia.set_master_nodes(nodes) 59 | 60 | @doc """ 61 | The `:mnesia.backup_checkpoint/2` function is used in Mnesia to create a 62 | backup of the database at a specific checkpoint. 63 | 64 | This function allows you to specify a checkpoint name, and `Mnesia` 65 | will generate a backup that includes all changes up to that point. 66 | 67 | This is particularly useful for creating consistent backups of the 68 | database state at known good points, facilitating easier recovery in 69 | case of data corruption or loss. 70 | 71 | > The tables are backed up to external media using backup module BackupMod. 72 | Tables with the local contents property are backed up as they exist on 73 | the current node. BackupMod is the default backup callback module obtained 74 | by `mnesia:system_info(backup_module)`. For information about the exact 75 | callback interface (the mnesia_backup behavior), see the User's Guide. 76 | 77 | ```erlang 78 | backup_checkpoint(Name, Dest) -> result() 79 | backup_checkpoint(Name, Dest, Mod) -> result() 80 | ``` 81 | 82 | ### Example 83 | 84 | ```elixir 85 | MnesiaAssistant.BackupAndRestore.backup_checkpoint(name, dest, mod) 86 | MnesiaAssistant.BackupAndRestore.backup_checkpoint(name, dest) 87 | ``` 88 | """ 89 | def backup_checkpoint(name, dest, mod), do: Mnesia.backup_checkpoint(name, dest, mod) 90 | 91 | @doc """ 92 | Read `backup_checkpoint/3` document. 93 | """ 94 | def backup_checkpoint(name, dest), do: Mnesia.backup_checkpoint(name, dest) 95 | 96 | @doc """ 97 | With this function, you can get a support version of `mnesia`. 98 | 99 | > Activates a new checkpoint covering all Mnesia tables, 100 | > including the schema, with maximum degree of redundancy, 101 | > and performs a backup using `backup_checkpoint/2` and `backup_checkpoint/3`. The 102 | > default value of the backup callback module BackupMod is 103 | > obtained by `mnesia:system_info(backup_module)`. 104 | 105 | ### Example 106 | 107 | ```elixir 108 | MnesiaAssistant.BackupAndRestore.backup(backup_path) 109 | MnesiaAssistant.BackupAndRestore.backup(backup_path, module) 110 | ``` 111 | """ 112 | def backup(backup_path), do: Mnesia.backup(backup_path) 113 | 114 | @doc """ 115 | Read `backup/1` document. 116 | """ 117 | def backup(backup_path, module), do: Mnesia.backup(backup_path, module) 118 | 119 | @doc """ 120 | Iterates over a backup, either to transform it into a new backup, or read it. 121 | The arguments are explained briefly here. For details, see the User's Guide. 122 | 123 | * SourceMod and TargetMod are the names of the modules that actually 124 | access the backup media. 125 | * Source and Target are opaque data used exclusively by modules SourceMod and 126 | TargetMod to initialize the backup media. 127 | * Acc is an initial accumulator value. 128 | * Fun(BackupItems, Acc) is applied to each item in the backup. 129 | The Fun must return a tuple {BackupItems,NewAcc}, where BackupItems is a list of valid backup items, and NewAcc is a new accumulator value. The returned backup items are written in the target backup. 130 | * LastAcc is the last accumulator value. This is the last NewAcc value 131 | that was returned by Fun. 132 | 133 | > The `:mnesia.traverse_backup/4` function in Mnesia is designed for traversing 134 | > and processing the contents of a `Mnesia` backup file. 135 | > This functionality is particularly useful when you need to inspect, 136 | > analyze, or selectively restore data from a backup. 137 | > The `traverse_backup` function allows you to specify callback functions 138 | > that will be called with the data from the backup, enabling you to 139 | > programmatically interact with the backup contents. 140 | 141 | ```erlang 142 | traverse_backup(Src :: term(), Dest :: term(), Fun, Acc) -> 143 | traverse_backup(Src :: term(), SrcMod :: module(), Dest :: term(), DestMod :: module(), Fun, Acc) 144 | ``` 145 | 146 | ### Example: 147 | 148 | ```elixir 149 | initial_acc = [] # Initial value for the accumulator 150 | 151 | MnesiaAssistant.BackupAndRestore.traverse_backup( 152 | source, 153 | dest, 154 | &BackupTraversal.table_fun/2, 155 | initial_acc 156 | ) 157 | # OR 158 | MnesiaAssistant.BackupAndRestore.traverse_backup( 159 | source, 160 | source_module, 161 | dest, 162 | dest_module 163 | &BackupTraversal.table_fun/2, 164 | initial_acc 165 | ) 166 | ``` 167 | """ 168 | def traverse_backup(source, dest, fun, acc), do: Mnesia.traverse_backup(source, dest, fun, acc) 169 | 170 | @doc """ 171 | Read `traverse_backup/4` document. 172 | """ 173 | def traverse_backup(source, source_module, dest, dest_module, fun, acc), 174 | do: Mnesia.traverse_backup(source, source_module, dest, dest_module, fun, acc) 175 | 176 | @doc """ 177 | With this function, tables can be restored online from a backup 178 | without restarting Mnesia. Opaque is forwarded to the backup module. 179 | `args` is a list of the following tuples: 180 | 181 | * `Op`: skip_tables | clear_tables | keep_tables | restore_tables 182 | * `Arg`: {module, module()} | {Op, [table()]} | {default_op, Op} 183 | 184 | --- 185 | 186 | 1. `{module,BackupMod}`. The backup module BackupMod is used to 187 | access the backup media. If omitted, the default backup module is used. 188 | 2. `{skip_tables, TabList}`, where TabList is a list of tables that is 189 | not to be read from the backup. 190 | 3. `{clear_tables, TabList}`, where TabList is a list of tables that 191 | is to be cleared before the records from the backup are inserted. 192 | That is, all records in the tables are deleted before the tables are restored. 193 | Schema information about the tables is not cleared or read from the backup. 194 | 4. `{keep_tables, TabList}`, where TabList is a list of tables that is not 195 | to be cleared before the records from the backup are inserted. 196 | That is, the records in the backup are added to the records in the table. 197 | Schema information about the tables is not cleared or read from the backup. 198 | 5. `{recreate_tables, TabList}`, where TabList is a list of tables that is 199 | to be recreated before the records from the backup are inserted. 200 | The tables are first deleted and then created with the schema information 201 | from the backup. All the nodes in the backup need to be operational. 202 | 6. `{default_op, Operation}`, where Operation is either of the operations `skip_tables`, 203 | `clear_tables`, `keep_tables`, or `recreate_tables`. 204 | The default operation specifies which operation that is to be used on 205 | tables from the backup that is not specified in any of the mentioned lists. 206 | If omitted, operation clear_tables is used. 207 | 208 | > The affected tables are `write-locked` during the restoration. 209 | > However, regardless of the lock conflicts caused by this, 210 | > the applications can continue to do their work while the restoration 211 | > is being performed. The restoration is performed as one single transaction. 212 | 213 | > If the database is huge, it it not always possible to restore it online. 214 | > In such cases, restore the old database by installing a fallback and then restart. 215 | 216 | ### Example: 217 | 218 | ```elixir 219 | MnesiaAssistant.BackupAndRestore.restore( 220 | "/tmp/mnesia_backup", [{:skip_tables, [Person]}] 221 | ) 222 | ``` 223 | """ 224 | def restore(source, args), do: Mnesia.restore(source, args) 225 | 226 | @doc """ 227 | Installs a backup as fallback. The fallback is used to restore the database 228 | at the next startup. Installation of fallbacks requires Erlang to be operational 229 | on all the involved nodes, but it does not matter if Mnesia is running or not. 230 | The installation of the fallback fails if the local node is 231 | not one of the `disc-resident` nodes in the backup. 232 | 233 | ### Example: 234 | 235 | ```elixir 236 | MnesiaAssistant.BackupAndRestore.install_fallback(source) 237 | # --> :mnesia.install_fallback(source) 238 | MnesiaAssistant.BackupAndRestore.install_fallback(source, module) 239 | # --> :mnesia.install_fallback(source, module) 240 | ``` 241 | """ 242 | def install_fallback(source), do: Mnesia.install_fallback(source) 243 | 244 | @doc """ 245 | Read `install_fallback/1` document. 246 | """ 247 | def install_fallback(source, module), do: Mnesia.install_fallback(source, module) 248 | 249 | @doc """ 250 | Deinstalls a fallback before it has been used to restore the database. 251 | This is normally a distributed operation that is either performed 252 | on all nodes with disc resident schema, or none. 253 | 254 | Uninstallation of fallbacks requires Erlang to be operational on all 255 | involved nodes, but it does not matter if Mnesia is running or not. 256 | Which nodes that are considered as disc-resident nodes is determined 257 | from the schema information in the local fallback. 258 | 259 | ```erlang 260 | Args = [{mnesia_dir, Dir :: string()}] 261 | {module, BackupMod}. For semantics, see mnesia:install_fallback/2. 262 | {scope, Scope}. For semantics, see mnesia:install_fallback/2. 263 | {mnesia_dir, AlternateDir}. For semantics, see mnesia:install_fallback/2. 264 | ``` 265 | 266 | ### Example: 267 | 268 | ```elixir 269 | MnesiaAssistant.BackupAndRestore.uninstall_fallback() 270 | # --> :mnesia.uninstall_fallback() 271 | MnesiaAssistant.BackupAndRestore.uninstall_fallback(args) 272 | # --> :mnesia.uninstall_fallback(args) 273 | ``` 274 | """ 275 | def uninstall_fallback(), do: Mnesia.uninstall_fallback() 276 | 277 | @doc """ 278 | Read `uninstall_fallback/0` document. 279 | """ 280 | def uninstall_fallback(args) when is_list(args), do: Mnesia.uninstall_fallback(args) 281 | 282 | @doc """ 283 | Performs a user-initiated dump of the local log file. 284 | This is usually not necessary, as Mnesia by default manages this automatically 285 | 286 | ### Example: 287 | 288 | ```elixir 289 | MnesiaAssistant.BackupAndRestore.dump_log() # --> :mnesia.dump_log() 290 | ``` 291 | """ 292 | def dump_log(), do: Mnesia.dump_log() 293 | 294 | @doc """ 295 | Dumps a set of `ram_copies` tables to disc. The next time the system is started, 296 | these tables are initiated with the data found in the files that are the 297 | result of this dump. None of the tables can have `disc-resident` replicas. 298 | 299 | ### Example: 300 | 301 | ```elixir 302 | MnesiaAssistant.BackupAndRestore.dump_tables(tables) # --> :mnesia.dump_tables(tables) 303 | ``` 304 | """ 305 | def dump_tables(tables) when is_list(tables), do: Mnesia.dump_tables(tables) 306 | end 307 | -------------------------------------------------------------------------------- /lib/modules/mnesia_assistant/operation/information.ex: -------------------------------------------------------------------------------- 1 | defmodule MnesiaAssistant.Information do 2 | @moduledoc """ 3 | This particular module is actually a set of functions in `Mnesia` that are designed 4 | to supply the user with the information that they require regarding the system. 5 | """ 6 | alias :mnesia, as: Mnesia 7 | @debug_level_types [:none, :verbose, :debug, :trace, false, true] 8 | 9 | @system_types [ 10 | :all, 11 | :access_module, 12 | :auto_repair, 13 | :backup_module, 14 | :checkpoints, 15 | :event_module, 16 | :db_nodes, 17 | :debug, 18 | :directory, 19 | :dump_log_load_regulation, 20 | :dump_log_time_threshold, 21 | :dump_log_update_in_place, 22 | :dump_log_write_threshold, 23 | :extra_db_nodes, 24 | :fallback_activated, 25 | :held_locks, 26 | :is_running, 27 | :local_tables, 28 | :lock_queue, 29 | :log_version, 30 | :master_node_tables, 31 | :protocol_version, 32 | :running_db_nodes, 33 | :schema_location, 34 | :subscribers, 35 | :tables, 36 | :transactions, 37 | :transaction_failures, 38 | :transaction_commits, 39 | :transaction_restarts, 40 | :transaction_log_writes, 41 | :use_dir, 42 | :version 43 | ] 44 | 45 | @doc """ 46 | This function is actually a combination of two functions 47 | in mnesia(`:mnesia.info()`, `:mnesia.schema()`). 48 | 49 | ### Example: 50 | 51 | ```elixir 52 | MnesiaAssistant.Information.info() 53 | # OR 54 | MnesiaAssistant.Information.info({:system, type}) 55 | # OR 56 | MnesiaAssistant.Information.info(type) 57 | ``` 58 | 59 | All types you can use: 60 | ``` 61 | [ 62 | :all, 63 | :access_module, 64 | :auto_repair, 65 | :backup_module, 66 | :checkpoints, 67 | :event_module, 68 | :db_nodes, 69 | :debug, 70 | :directory, 71 | :dump_log_load_regulation, 72 | :dump_log_time_threshold, 73 | :dump_log_update_in_place, 74 | :dump_log_write_threshold, 75 | :extra_db_nodes, 76 | :fallback_activated, 77 | :held_locks, 78 | :is_running, 79 | :local_tables, 80 | :lock_queue, 81 | :log_version, 82 | :master_node_tables, 83 | :protocol_version, 84 | :running_db_nodes, 85 | :schema_location, 86 | :subscribers, 87 | :tables, 88 | :transactions, 89 | :transaction_failures, 90 | :transaction_commits, 91 | :transaction_restarts, 92 | :transaction_log_writes, 93 | :use_dir, 94 | :version, 95 | :schema 96 | ] 97 | ``` 98 | """ 99 | def info(), do: Mnesia.info() 100 | 101 | @doc """ 102 | Read `info/0` document. 103 | """ 104 | def info({:system, type}) when type in @system_types, do: Mnesia.system_info(type) 105 | 106 | def info(type) when type in @system_types, do: Mnesia.system_info(type) 107 | 108 | def info(:schema), do: Mnesia.schema() 109 | 110 | @doc """ 111 | Read `info/0` document. 112 | """ 113 | def system_info(type) when is_atom(type), do: Mnesia.system_info(type) 114 | 115 | @doc """ 116 | If the application detects a communication failure (in a potentially partitioned network) 117 | that can have caused an inconsistent database, it can use the function 118 | `mnesia:set_master_nodes(Tab, MasterNodes)` to define from which nodes 119 | each table is to be loaded. At startup, the Mnesia normal table load algorithm is 120 | bypassed and the table is loaded from one of the master nodes defined for the table, 121 | regardless of when and if Mnesia terminated on other nodes. MasterNodes can only 122 | contain nodes where the table has a replica. If the MasterNodes list is empty, 123 | the master node recovery mechanism for the particular table is reset, 124 | and the normal load mechanism is used at the next restart. 125 | 126 | > The master node setting is always local. It can be changed regardless 127 | > if Mnesia is started or not. 128 | 129 | The database can also become inconsistent if configuration 130 | parameter `max_wait_for_decision` is used or if `mnesia:force_load_table/1` is used. 131 | 132 | ### Example: 133 | 134 | ```elixir 135 | MnesiaAssistant.Information.set_debug_level(level) 136 | ``` 137 | 138 | All level you can use: `[:none, :verbose, :debug, :trace, false, true]` 139 | """ 140 | def set_debug_level(level) when level in @debug_level_types, 141 | do: Mnesia.set_debug_level(level) 142 | 143 | @doc """ 144 | Ensures that the local transaction log file is synced to disk. 145 | On a single node system, data written to disk tables since the 146 | last dump can be lost if there is a power outage. 147 | 148 | ### Example: 149 | 150 | ```elixir 151 | MnesiaAssistant.Information.sync_log() 152 | ``` 153 | """ 154 | def sync_log(), do: Mnesia.sync_log() 155 | end 156 | -------------------------------------------------------------------------------- /lib/modules/mnesia_assistant/operation/query.ex: -------------------------------------------------------------------------------- 1 | defmodule MnesiaAssistant.Query do 2 | @moduledoc """ 3 | **Querying** is one of the most significant aspects of working with the **database**; 4 | for this reason, you can access the aggregation functions of `Mnesia` 5 | in this section of the database. 6 | """ 7 | alias :mnesia, as: Mnesia 8 | alias MishkaDeveloperTools.Helper.Extra 9 | @table_lock_types [:read, :write, :sticky_write] 10 | 11 | @doc """ 12 | The database can be accessed through the use of this function to retrieve a record. 13 | It is important to remind you that if you are seeking for a 14 | reliable assurance for the retrieval of data, you can utilise transaction. 15 | 16 | ### Example: 17 | 18 | ```elixir 19 | MnesiaAssistant.Transaction.transaction(fn -> 20 | MnesiaAssistant.Query.read({Person, 20}) 21 | end) 22 | # OR 23 | MnesiaAssistant.Query.read(Person, 20) 24 | # OR use one of @table_lock_types [:read, :write, :sticky_write] 25 | MnesiaAssistant.Query.read(Person, 20, lock_type) 26 | ``` 27 | 28 | > Reads all records from table Tab with key Key. 29 | > This function has the same semantics regardless of the location of Tab. 30 | > If the table is of type `bag`, the function `mnesia:read(Tab, Key)` can return 31 | > an arbitrarily long list. If the table is of type set, the list 32 | > is either of length `1`, or `[]`. 33 | 34 | **Note**: The LockKind ([:read, :write, :sticky_write]) argument is used to control the 35 | locking behavior during the read operation, which can influence data consistency and 36 | concurrency control for the operation. 37 | """ 38 | def read({table, key}), do: Mnesia.read(table, key) 39 | 40 | def read(table, key), do: Mnesia.read(table, key) 41 | 42 | def read(table, key, lock_type) when lock_type in @table_lock_types, 43 | do: Mnesia.read(table, key, lock_type) 44 | 45 | # mnesia:read(Tab, Key, write) 46 | @doc """ 47 | It is like `read/3` with `:write` LockKind. 48 | 49 | ### Example: 50 | 51 | ```elixir 52 | MnesiaAssistant.Query.wread(Person, 20) 53 | ``` 54 | """ 55 | def wread({table, key}), do: Mnesia.wread({table, key}) 56 | 57 | # mnesia:index_read(person, 36, age) 58 | @doc """ 59 | Assume that there is an index on position Pos for a certain record type. 60 | This function can be used to read the records without knowing the actual `key` 61 | for the record. For example, with an index in position 1 of table person, 62 | the call `mnesia:index_read(person, 36, #person.age)` returns a list of all 63 | persons with age 36. Pos can also be an attribute name (atom), 64 | but if the notation `mnesia:index_read(person, 36, age)` is used, 65 | the field position is searched for in runtime, for each call. 66 | 67 | ### Example: 68 | 69 | ```elixir 70 | MnesiaAssistant.Query.index_read(Person, 20, :age) 71 | ``` 72 | """ 73 | def index_read(table, key, attr), do: Mnesia.index_read(table, key, attr) 74 | 75 | @doc """ 76 | Returns first record of the table concerned. 77 | 78 | ### Example: 79 | 80 | ```elixir 81 | MnesiaAssistant.Query.first(Person) 82 | ``` 83 | """ 84 | def first(table), do: Mnesia.first(table) |> check_nil_end_of_table() 85 | 86 | @doc """ 87 | Returns last record of the table concerned. 88 | 89 | ### Example: 90 | 91 | ```elixir 92 | MnesiaAssistant.Query.last(Person) 93 | ``` 94 | """ 95 | def last(table), do: Mnesia.last(table) |> check_nil_end_of_table() 96 | 97 | @doc """ 98 | Returns next record of the table concerned from your selected :id as the key. 99 | 100 | ### Example: 101 | 102 | ```elixir 103 | MnesiaAssistant.Query.next(Person, 1) 104 | ``` 105 | """ 106 | def next(table, key), do: Mnesia.next(table, key) |> check_nil_end_of_table() 107 | 108 | @doc """ 109 | Returns prev record of the table concerned from your selected :id as the key. 110 | 111 | ### Example: 112 | 113 | ```elixir 114 | MnesiaAssistant.Query.prev(Person, 1) 115 | ``` 116 | """ 117 | def prev(table, key), do: Mnesia.prev(table, key) |> check_nil_end_of_table() 118 | 119 | @doc """ 120 | By using this function, you will be able to delete a record from 121 | the table based on the key that you desire. 122 | 123 | **Note**: The LockKind ([:write, :sticky_write]) argument is used to control the 124 | locking behavior during the delete operation, which can influence data consistency and 125 | concurrency control for the operation. 126 | 127 | ### Example: 128 | 129 | ```elixir 130 | MnesiaAssistant.Query.delete(Person, 1, :write) 131 | # OR 132 | MnesiaAssistant.Query.delete(Person, 1) 133 | ``` 134 | """ 135 | def delete(table, key, lock_type) when lock_type in [:write, :sticky_write] do 136 | Mnesia.delete(table, key, lock_type) 137 | end 138 | 139 | @doc """ 140 | Read `delete/3` document. 141 | """ 142 | def delete(table, key), do: Mnesia.delete({table, key}) 143 | 144 | @doc """ 145 | To use `:mnesia.delete_object/3` in Elixir, you need to specify the table from 146 | which to delete (Tab), the complete record to be deleted (Rec), 147 | and the lock type (LockKind). This function is particularly useful 148 | when you need to delete a specific record and you know the entire tuple 149 | structure of that record. 150 | 151 | ### Example: 152 | 153 | ```elixir 154 | record_to_delete = {Person, 2, "John Doe", 30} 155 | 156 | MnesiaAssistant.Query.delete_object(record_to_delete) 157 | # OR 158 | MnesiaAssistant.Query.delete_object(Person, record_to_delete, :write) 159 | ``` 160 | """ 161 | def delete_object(record) when is_tuple(record), do: Mnesia.delete_object(record) 162 | 163 | @doc """ 164 | Read `delete_object/1` document. 165 | """ 166 | def delete_object(table, record, lock_type) 167 | when is_tuple(record) and lock_type in [:write, :sticky_write], 168 | do: Mnesia.delete_object(table, record, lock_type) 169 | 170 | @doc """ 171 | It is like `MnesiaAssistant.Query.delete(table, key, :sticky_write)`. 172 | For more information read `delete/3` or `mnesia:delete(Tab, Key, sticky_write)`. 173 | 174 | ### Example: 175 | 176 | ```elixir 177 | MnesiaAssistant.Query.s_delete(Person, 20) 178 | ``` 179 | """ 180 | def s_delete({table, key}), do: Mnesia.s_delete({table, key}) 181 | 182 | @doc """ 183 | It is like `MnesiaAssistant.Query.delete_object(table, record, :sticky_write)` 184 | where Tab is element(1, Record). 185 | For more information read `delete_object/3` or `mnesia:delete_object(Tab, Record, sticky_write)`. 186 | 187 | ### Example: 188 | 189 | ```elixir 190 | MnesiaAssistant.Query.s_delete_object({Person, 2, "John Doe", 30}) 191 | ``` 192 | """ 193 | def s_delete_object(record), do: Mnesia.s_delete_object(record) 194 | 195 | @doc """ 196 | The following function will help you update a record of a table 197 | based on a specific key. 198 | 199 | **Note**: The LockKind ([:write, :sticky_write]) argument is used to control the 200 | locking behavior during the write operation, which can influence data consistency and 201 | concurrency control for the operation. 202 | 203 | ### Example: 204 | 205 | ```elixir 206 | updated_person = {Person, id, name, new_age} 207 | 208 | MnesiaAssistant.Query.write(Person, updated_person) 209 | # OR 210 | MnesiaAssistant.Query.write(Person, updated_person, :write) 211 | # OR 212 | MnesiaAssistant.Query.write(updated_person) 213 | ``` 214 | """ 215 | def write(table, record, lock_type \\ :write) when lock_type in [:write, :sticky_write] do 216 | Mnesia.write(table, record, lock_type) 217 | end 218 | 219 | @doc """ 220 | Read `write/3` document. 221 | """ 222 | def write(record), do: Mnesia.write(record) 223 | 224 | @doc """ 225 | It is like `MnesiaAssistant.Query.write(Person, updated_person, :sticky_write)` or 226 | `mnesia:write(Tab, Record, sticky_write)`. 227 | For more information read `write/3`. 228 | 229 | ### Example: 230 | 231 | ```elixir 232 | updated_person = {Person, id, name, new_age} 233 | 234 | MnesiaAssistant.Query.s_write(updated_person) 235 | ``` 236 | """ 237 | def s_write(record), do: Mnesia.s_write(record) 238 | 239 | @doc """ 240 | The `select/5` function allows you to specify a custom query using any 241 | operator or function in the Elixir language (or Erlang for that matter). 242 | 243 | ### Example: 244 | 245 | ```elixir 246 | alias MnesiaAssistant.Query 247 | 248 | Query.select(Person, [{{Person, :"$1", :"$2", :"$3"}, [{:>, :"$1", 3}], [:"$$"]}]) 249 | # OR you can use this helper `select/5` 250 | Query.select( 251 | Person, 252 | [:"$1", :"$2", :"$3"], 253 | [{:>, :"$1", 3}], 254 | :write, 255 | [result_type: [:"$$"], lock_type: :write] 256 | ) 257 | ``` 258 | """ 259 | def select(table, match_fields, conds, opts, :custom) 260 | when is_list(match_fields) and is_list(conds) do 261 | result_type = Keyword.get(opts, :result_type, [:"$$"]) 262 | lock_type = Keyword.get(opts, :lock_type) 263 | converted = select_converter(table, match_fields, conds, result_type) 264 | 265 | cond do 266 | is_nil(lock_type) -> Mnesia.select(table, converted, lock_type) 267 | !is_nil(lock_type) and lock_type in @table_lock_types -> Mnesia.select(table, converted) 268 | true -> raise "The input of the select function is wrong. Do according to the document" 269 | end 270 | end 271 | 272 | @doc """ 273 | Read `select/5` document. 274 | """ 275 | def select(table, spec, limit, lock_type) when lock_type in @table_lock_types do 276 | Mnesia.select(table, spec, limit, lock_type) 277 | end 278 | 279 | @doc """ 280 | Read `select/5` document. 281 | """ 282 | def select(table, spec), do: Mnesia.select(table, spec) 283 | 284 | @doc """ 285 | Read `select/5` document. 286 | """ 287 | def select(table, spec, lock_type) when lock_type in @table_lock_types do 288 | Mnesia.select(table, spec, lock_type) 289 | end 290 | 291 | @doc """ 292 | Read `select/5` document. 293 | """ 294 | def select(cont), do: Mnesia.select(cont) 295 | 296 | @doc """ 297 | There is a distinction between this method and the `select` function, 298 | which is that you are aware of the record that you wish to remove from the table. 299 | It is like: `:mnesia.match_object({Person, :_, "Mishka", :_})` 300 | 301 | ### Example: 302 | 303 | ```elixir 304 | alias MnesiaAssistant.Query 305 | 306 | Query.match_object(Person, [:_, "Mishka", :_]) 307 | # OR 308 | Query.match_object(Person, [:_, "Mishka", :_], :write) 309 | # OR 310 | Query.match_object(Person, {Person, :_, "Mishka", :_}, :write) 311 | # OR 312 | Query.match_object({Person, :_, "Mishka", :_}) 313 | ``` 314 | """ 315 | def match_object(table, pattern) when is_list(pattern) do 316 | pattern = ([table] ++ [pattern]) |> List.to_tuple() 317 | Mnesia.match_object(pattern) 318 | end 319 | 320 | @doc """ 321 | Read `match_object/2` document. 322 | """ 323 | def match_object(table, pattern, lock_type) 324 | when is_list(pattern) and lock_type in @table_lock_types do 325 | pattern = ([table] ++ [pattern]) |> List.to_tuple() 326 | Mnesia.match_object(table, pattern, lock_type) 327 | end 328 | 329 | def match_object(table, pattern, lock_type) 330 | when is_tuple(pattern) and lock_type in @table_lock_types do 331 | Mnesia.match_object(table, pattern, lock_type) 332 | end 333 | 334 | @doc """ 335 | Read `match_object/2` document. 336 | """ 337 | def match_object(pattern), do: Mnesia.match_object(pattern) 338 | 339 | # Fun = fun({account, _AccountID, Balance}, Acc) -> Acc + Balance end, 340 | # InitialAcc = 0 341 | # mnesia:foldl(Fun, InitialAcc, account) end) 342 | @doc """ 343 | it is like `Enum.reduce`. 344 | 345 | ### Example: 346 | 347 | ```elixir 348 | initial_acc = 0 349 | 350 | fun = fn {:people, _id, _name, age}, acc -> 351 | acc + age 352 | end 353 | 354 | MnesiaAssistant.Query.foldl(Person, initial_acc, fun) 355 | # OR 356 | MnesiaAssistant.Query.foldl(Person, initial_acc, fun, :write) 357 | # OR 358 | MnesiaAssistant.Transaction.transaction(fn -> 359 | MnesiaAssistant.Query.foldl(Person, initial_acc, fun) 360 | end) 361 | ``` 362 | """ 363 | def foldl(table, initial_acc, foldl_fun) when is_function(foldl_fun) do 364 | Mnesia.foldl(foldl_fun, initial_acc, table) 365 | end 366 | 367 | @doc """ 368 | Read `foldl/3` document. 369 | """ 370 | def foldl(table, initial_acc, foldl_fun, lock_type) 371 | when is_function(foldl_fun) and lock_type in @table_lock_types do 372 | Mnesia.foldl(foldl_fun, initial_acc, table, lock_type) 373 | end 374 | 375 | @doc """ 376 | Works exactly like foldl/3 but iterates the table in the opposite order for the `ordered_set` 377 | table type. For all other table types, `foldr/3` and `foldl/3` are synonyms. 378 | 379 | Read `foldl/3` document. 380 | """ 381 | def foldr(table, initial_acc, foldr_fun) when is_function(foldr_fun) do 382 | Mnesia.foldr(foldr_fun, initial_acc, table) 383 | end 384 | 385 | @doc """ 386 | Read `foldr/3` document. 387 | """ 388 | def foldr(table, initial_acc, foldr_fun, lock_type) 389 | when is_function(foldr_fun) and lock_type in @table_lock_types do 390 | Mnesia.foldr(foldr_fun, initial_acc, table, lock_type) 391 | end 392 | 393 | @doc """ 394 | In a manner similar to the function `mnesia:index_read/3`, any index information can 395 | be used when trying to match records. This function takes a pattern that 396 | obeys the same rules as the function `mnesia:match_object/3`, except that this 397 | function requires the following conditions: 398 | 399 | * The table Tab must have an index on position Pos. 400 | * The element in position Pos in Pattern must be bound. Pos is an integer (`#record.Field`) or an attribute name. 401 | 402 | > The two index search functions described here are automatically 403 | > started when searching tables with qlc list comprehensions and 404 | > also when using the low-level `mnesia:[dirty_]match_object` functions. 405 | 406 | The semantics of this function is context-sensitive. For details, 407 | `see mnesia:activity/4`. In transaction-context, it acquires a lock of type 408 | LockKind on the entire table or on a single record. Currently, 409 | the lock type read is supported. 410 | 411 | ### Example: 412 | 413 | ```elixir 414 | MnesiaAssistant.Query.index_match_object({Person, :_, "Mishka", :_}, 30) 415 | MnesiaAssistant.Query.index_match_object(Person, {Person, :_, "Mishka", :_}, 30, :write) 416 | ``` 417 | """ 418 | def index_match_object(pattern, index_attr) when is_tuple(pattern), 419 | do: Mnesia.index_match_object(pattern, index_attr) 420 | 421 | def index_match_object(table, pattern, index_attr, lock_type) 422 | when lock_type in @table_lock_types and is_tuple(pattern), 423 | do: Mnesia.index_match_object(table, pattern, index_attr, lock_type) 424 | 425 | @doc """ 426 | There is a distinction between this method and the `select` function, 427 | which is that you are aware of the record that you wish to remove from the table. 428 | It is like: `:mnesia.match_object({Person, :_, "Mishka", :_})` 429 | 430 | ### Example: 431 | 432 | ```elixir 433 | alias MnesiaAssistant.Query 434 | 435 | Query.dirty_match_object(Person, [:_, "Mishka", :_]) 436 | # OR 437 | Query.dirty_match_object(Person, {Person, :_, "Mishka", :_}) 438 | # OR 439 | Query.dirty_match_object({Person, :_, "Mishka", :_}) 440 | ``` 441 | 442 | > **Note**: In the context of `mnesia`, the `dirty` keyword indicates that it 443 | > encompasses the maximum speed, but it does not guarantee that it will execute correctly. 444 | """ 445 | def dirty_match_object(table, pattern) when is_list(pattern) do 446 | pattern = ([table] ++ [pattern]) |> List.to_tuple() 447 | Mnesia.dirty_match_object(pattern) 448 | end 449 | 450 | def dirty_match_object(table, pattern) when is_tuple(pattern) do 451 | Mnesia.dirty_match_object(table, pattern) 452 | end 453 | 454 | @doc """ 455 | Read `dirty_match_object/2` document. 456 | """ 457 | def dirty_match_object(pattern) when is_tuple(pattern), do: Mnesia.dirty_match_object(pattern) 458 | 459 | @doc """ 460 | The database can be accessed through the use of this function to retrieve a record. 461 | It is important to remind you that if you are seeking for a 462 | reliable assurance for the retrieval of data, you can utilise transaction. 463 | 464 | > **Note**: In the context of `mnesia`, the `dirty` keyword indicates that it 465 | > encompasses the maximum speed, but it does not guarantee that it will execute correctly. 466 | 467 | ### Example: 468 | 469 | ```elixir 470 | alias MnesiaAssistant.Query 471 | 472 | Query.dirty_read(Person, 20) 473 | ``` 474 | """ 475 | def dirty_read(module, key), do: Mnesia.dirty_read(module, key) 476 | 477 | @doc """ 478 | Dirty equivalent of the function `index_read/3`. 479 | 480 | > **Note**: In the context of `mnesia`, the `dirty` keyword indicates that it 481 | > encompasses the maximum speed, but it does not guarantee that it will execute correctly. 482 | 483 | ### Example: 484 | 485 | ```elixir 486 | MnesiaAssistant.Query.dirty_index_read(Person, 20, :age) 487 | ``` 488 | 489 | > Attr = index_attr() 490 | """ 491 | def dirty_index_read(table, key, attr), do: Mnesia.dirty_index_read(table, key, attr) 492 | 493 | @doc """ 494 | Dirty equivalent of the function `first/1`. 495 | 496 | > **Note**: In the context of `mnesia`, the `dirty` keyword indicates that it 497 | > encompasses the maximum speed, but it does not guarantee that it will execute correctly. 498 | 499 | ### Example: 500 | 501 | ```elixir 502 | MnesiaAssistant.Query.dirty_first(Person) 503 | ``` 504 | """ 505 | def dirty_first(table), do: Mnesia.dirty_first(table) |> check_nil_end_of_table() 506 | 507 | @doc """ 508 | Dirty equivalent of the function `last/1`. 509 | 510 | > **Note**: In the context of `mnesia`, the `dirty` keyword indicates that it 511 | > encompasses the maximum speed, but it does not guarantee that it will execute correctly. 512 | 513 | ### Example: 514 | 515 | ```elixir 516 | MnesiaAssistant.Query.dirty_last(Person) 517 | ``` 518 | """ 519 | def dirty_last(table), do: Mnesia.dirty_last(table) |> check_nil_end_of_table() 520 | 521 | @doc """ 522 | Dirty equivalent of the function `next/2`. 523 | 524 | > **Note**: In the context of `mnesia`, the `dirty` keyword indicates that it 525 | > encompasses the maximum speed, but it does not guarantee that it will execute correctly. 526 | 527 | ### Example: 528 | 529 | ```elixir 530 | MnesiaAssistant.Query.dirty_next(Person, 20) 531 | ``` 532 | """ 533 | def dirty_next(table, key), do: Mnesia.dirty_next(table, key) |> check_nil_end_of_table() 534 | 535 | @doc """ 536 | Dirty equivalent of the function `prev/2`. 537 | 538 | > **Note**: In the context of `mnesia`, the `dirty` keyword indicates that it 539 | > encompasses the maximum speed, but it does not guarantee that it will execute correctly. 540 | 541 | ### Example: 542 | 543 | ```elixir 544 | MnesiaAssistant.Query.dirty_prev(Person, 20) 545 | ``` 546 | """ 547 | def dirty_prev(table, key), do: Mnesia.dirty_prev(table, key) |> check_nil_end_of_table() 548 | 549 | @doc """ 550 | Dirty equivalent of the function `delete/2`. 551 | 552 | > **Note**: In the context of `mnesia`, the `dirty` keyword indicates that it 553 | > encompasses the maximum speed, but it does not guarantee that it will execute correctly. 554 | 555 | ### Example: 556 | 557 | ```elixir 558 | MnesiaAssistant.Query.dirty_delete(Person, 20) 559 | # OR 560 | MnesiaAssistant.Query.dirty_delete({Person, 20}) 561 | ``` 562 | """ 563 | def dirty_delete(table, key), do: Mnesia.dirty_delete(table, key) 564 | 565 | @doc """ 566 | Read `dirty_delete/1` document. 567 | """ 568 | def dirty_delete({table, key}), do: Mnesia.dirty_delete(table, key) 569 | 570 | @doc """ 571 | Dirty equivalent of the function `delete_object/1`. 572 | 573 | > **Note**: In the context of `mnesia`, the `dirty` keyword indicates that it 574 | > encompasses the maximum speed, but it does not guarantee that it will execute correctly. 575 | 576 | ### Example: 577 | 578 | ```elixir 579 | Query.dirty_delete_object({Person, :_, "Mishka", :_}) 580 | ``` 581 | """ 582 | def dirty_delete_object(record) when is_tuple(record), do: Mnesia.dirty_delete_object(record) 583 | 584 | @doc """ 585 | Read `dirty_delete_object/1` document. 586 | """ 587 | def dirty_delete_object(table, record) when is_tuple(record), 588 | do: Mnesia.dirty_delete_object(table, record) 589 | 590 | @doc """ 591 | Dirty equivalent of the function `write/1`. 592 | 593 | > **Note**: In the context of `mnesia`, the `dirty` keyword indicates that it 594 | > encompasses the maximum speed, but it does not guarantee that it will execute correctly. 595 | 596 | ### Example: 597 | 598 | ```elixir 599 | updated_person = {Person, id, name, new_age} 600 | 601 | MnesiaAssistant.Query.dirty_write(Person, updated_person) 602 | # OR 603 | MnesiaAssistant.Query.dirty_write(updated_person) 604 | ``` 605 | """ 606 | def dirty_write(table, record), do: Mnesia.dirty_write(table, record) 607 | 608 | @doc """ 609 | Read `dirty_write/2` document. 610 | """ 611 | def dirty_write(record), do: Mnesia.dirty_write(record) 612 | 613 | @doc """ 614 | Dirty equivalent of the function `index_match_object/2`. 615 | 616 | > **Note**: In the context of `mnesia`, the `dirty` keyword indicates that it 617 | > encompasses the maximum speed, but it does not guarantee that it will execute correctly. 618 | 619 | ### Example: 620 | 621 | ```elixir 622 | MnesiaAssistant.Query.dirty_index_match_object({Person, :_, "Mishka", :_}, 30) 623 | MnesiaAssistant.Query.dirty_index_match_object(Person, {Person, :_, "Mishka", :_}, 30) 624 | ``` 625 | """ 626 | def dirty_index_match_object(pattern, index_attr), 627 | do: Mnesia.dirty_index_match_object(pattern, index_attr) 628 | 629 | @doc """ 630 | Read `dirty_index_match_object/2` document. 631 | """ 632 | def dirty_index_match_object(table, pattern, index_attr), 633 | do: Mnesia.dirty_index_match_object(table, pattern, index_attr) 634 | 635 | @doc """ 636 | Dirty equivalent of the function `select/5`. 637 | 638 | > **Note**: In the context of `mnesia`, the `dirty` keyword indicates that it 639 | > encompasses the maximum speed, but it does not guarantee that it will execute correctly. 640 | 641 | ### Example: 642 | 643 | ```elixir 644 | MnesiaAssistant.Query.dirty_select(Person, [{{Person, :"$1", :"$2", :"$3"}, [{:>, :"$1", 3}], [:"$$"]}]) 645 | # OR 646 | MnesiaAssistant.Query.dirty_select( 647 | Person, 648 | [:"$1", :"$2", :"$3"], 649 | [{:>, :"$1", 3}], 650 | [:"$$"] 651 | ) 652 | ``` 653 | """ 654 | def dirty_select(table, match_fields, conds, result_type \\ [:"$$"]) do 655 | Mnesia.dirty_select(table, select_converter(table, match_fields, conds, result_type)) 656 | end 657 | 658 | @doc """ 659 | Read `dirty_select/4` document. 660 | """ 661 | def dirty_select(table, spec), do: Mnesia.dirty_select(table, spec) 662 | 663 | @doc """ 664 | Mnesia has no special counter records. However, records of the form `{Tab, Key, Integer}` 665 | can be used as (possibly `disc-resident`) counters when Tab is a set. 666 | This function updates a counter with a positive or negative number. 667 | However, counters can never become less than zero. There are two significant 668 | differences between this function and the action of first reading the record, 669 | performing the arithmetic, and then writing the record: 670 | 671 | 1. It is much more efficient. 672 | 2. `mnesia:dirty_update_counter/3` is performed as an atomic operation although it is 673 | not protected by a transaction. 674 | 675 | --- 676 | 677 | If two processes perform `mnesia:dirty_update_counter/3` simultaneously, 678 | both updates take effect without the risk of losing one of the updates. 679 | The new value NewVal of the counter is returned. 680 | 681 | > If Key does not exist, a new record is created with value Incr if it is larger than 0, 682 | > otherwise it is set to 0. 683 | 684 | > **Note**: In the context of `mnesia`, the `dirty` keyword indicates that it 685 | > encompasses the maximum speed, but it does not guarantee that it will execute correctly. 686 | 687 | ### Example: 688 | 689 | ```elixir 690 | MnesiaAssistant.Query.dirty_update_counter(:counters, :visits, 1) 691 | # OR+ 692 | MnesiaAssistant.Query.dirty_update_counter({:counters, :visits}, 1) 693 | ``` 694 | """ 695 | def dirty_update_counter({table, key}, incr) when is_integer(incr), 696 | do: Mnesia.dirty_update_counter({table, key}, incr) 697 | 698 | @doc """ 699 | Read `dirty_update_counter/2` document. 700 | """ 701 | def dirty_update_counter(table, key, incr) when is_integer(incr), 702 | do: Mnesia.dirty_update_counter(table, key, incr) 703 | 704 | @doc """ 705 | Calls the Fun in a context that is not protected by a transaction. 706 | The `Mnesia` function calls performed in the Fun are mapped to the corresponding 707 | dirty functions. It is performed in almost the same context as `mnesia:async_dirty/1,2`. 708 | The difference is that the operations are performed synchronously. 709 | The caller waits for the updates to be performed on all active replicas 710 | before the Fun returns. For details, see `mnesia:activity/4` and the User's Guide. 711 | 712 | > **Note**: In the context of `mnesia`, the `dirty` keyword indicates that it 713 | > encompasses the maximum speed, but it does not guarantee that it will execute correctly. 714 | 715 | ### Example: 716 | 717 | ```elixir 718 | MnesiaAssistant.Query.sync_dirty(fn -> something end) 719 | # OR 720 | MnesiaAssistant.Query.sync_dirty(fn -> something end, args) 721 | ``` 722 | """ 723 | def sync_dirty(sync_dirty_fn) when is_function(sync_dirty_fn), 724 | do: Mnesia.sync_dirty(sync_dirty_fn) 725 | 726 | @doc """ 727 | Read `sync_dirty/1` document. 728 | """ 729 | def sync_dirty(sync_dirty_fn, args) when is_function(sync_dirty_fn) and is_list(args) do 730 | Mnesia.sync_dirty(sync_dirty_fn, args) 731 | end 732 | 733 | @doc """ 734 | When this function is executed inside a transaction-context, it returns true, otherwise false. 735 | """ 736 | def is_transaction?(), do: Mnesia.is_transaction() 737 | 738 | defp check_nil_end_of_table(:"$end_of_table"), do: nil 739 | defp check_nil_end_of_table(value), do: value 740 | 741 | defp select_converter(table, match_fields, conds, result_type) do 742 | fields_pattern = ([table] ++ match_fields) |> List.to_tuple() 743 | 744 | conds = 745 | Enum.map(conds, fn {cond, field, value} -> {Extra.erlang_guard(cond), field, value} end) 746 | 747 | [{fields_pattern, conds, result_type}] 748 | end 749 | end 750 | -------------------------------------------------------------------------------- /lib/modules/mnesia_assistant/operation/schema.ex: -------------------------------------------------------------------------------- 1 | defmodule MnesiaAssistant.Schema do 2 | @moduledoc """ 3 | This module not only provides a number of tools to better cover a real project, 4 | but it also organises functions linked to Schema in a single location. 5 | """ 6 | alias :mnesia, as: Mnesia 7 | 8 | @doc """ 9 | By means of this function, you can see information about `Schema`. 10 | It should be noted that this function is the same as the 11 | `MnesiaAssistant.Information.info/1` function with the `:schema` input. 12 | 13 | ### Example: 14 | 15 | ```elixir 16 | MnesiaAssistant.Schema.schema() 17 | # OR 18 | MnesiaAssistant.Schema.schema(table) 19 | ``` 20 | """ 21 | def schema(), do: Mnesia.schema() 22 | 23 | def schema(table), do: Mnesia.schema(table) 24 | 25 | @doc """ 26 | You will need to establish a `schema` in order to begin utilising `Mnesia` 27 | in a project; this function will automatically create the schema for you. 28 | 29 | > Note that prior to the creation of the schema, it is necessary 30 | > to determine whether or not the storage path that was introduced 31 | > by the configuration already exists. Assuming that it does not already exist, 32 | > this path ought to be made. see more information `create_dir()/0` 33 | 34 | ### Example: 35 | 36 | ```elixir 37 | MnesiaAssistant.Schema.create_schema([node()]) 38 | ``` 39 | """ 40 | def create_schema(nodes \\ [node()]) when is_list(nodes), do: Mnesia.create_schema(nodes) 41 | 42 | @doc """ 43 | In the same way that you are able to generate a schema for a particular node, 44 | you are also able to delete it from your project. 45 | 46 | ### Example: 47 | 48 | ```elixir 49 | MnesiaAssistant.Schema.delete_schema([node()]) 50 | ``` 51 | """ 52 | def delete_schema(nodes \\ [node()]), do: Mnesia.delete_schema(nodes) 53 | 54 | @doc """ 55 | In Mnesia, the concept of `checkpoints` as it might be understood in other 56 | database systems (specific points in time to which you can revert the database state) 57 | is not directly exposed through a simple API call like `:mnesia.activate_checkpoint`. 58 | Mnesia's model for data recovery, consistency, and fault tolerance is built 59 | around its transactional model, replication, and `backup`/`restore` functionalities. 60 | 61 | ```erlang 62 | {name, Name} 63 | | {max, [table()]} 64 | | {min, [table()]} 65 | | {allow_remote, boolean()} 66 | | {ram_overrides_dump, boolean()} 67 | ``` 68 | 69 | ### Example: 70 | 71 | ```elixir 72 | MnesiaAssistant.Schema.activate_checkpoint(args) 73 | ``` 74 | """ 75 | def activate_checkpoint(args) when is_list(args), do: Mnesia.activate_checkpoint(args) 76 | 77 | @doc """ 78 | For deactivating `activate_checkpoint/1`. 79 | 80 | ### Example: 81 | 82 | ```elixir 83 | MnesiaAssistant.Schema.deactivate_checkpoint(name) 84 | ``` 85 | """ 86 | def deactivate_checkpoint(name), do: Mnesia.deactivate_checkpoint(name) 87 | 88 | @doc """ 89 | Using the `mnesia` configuration, this straightforward method will assist 90 | you in determining whether or not the path that you have supplied is present. 91 | When it does not already exist, it develops it. 92 | 93 | ### Example: 94 | 95 | ```elixir 96 | MnesiaAssistant.Schema.create_dir() 97 | ``` 98 | """ 99 | def create_dir() do 100 | path = Application.get_env(:mnesia, :dir) 101 | 102 | with true <- !is_nil(path), false <- File.dir?(path) do 103 | :ok = File.mkdir_p!(path) 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /lib/modules/mnesia_assistant/operation/snmp.ex: -------------------------------------------------------------------------------- 1 | defmodule MnesiaAssistant.Snmp do 2 | @moduledoc """ 3 | This module provides Simple Network Management Protocol (SNMP) functionality for mnesia. 4 | > TODO: This module needs to completely cover all of the functions that are 5 | > relevant to it. 6 | > In the event that you have mastered this section, kindly submit a Pull Request 7 | > for this section. 8 | """ 9 | alias :mnesia, as: Mnesia 10 | 11 | @doc """ 12 | TODO: Neet to be completed. 13 | """ 14 | def snmp_close_table(table), do: Mnesia.snmp_close_table(table) 15 | 16 | @doc """ 17 | TODO: Neet to be completed. 18 | """ 19 | def snmp_get_mnesia_key(table, rowIndex) when is_list(rowIndex), 20 | do: Mnesia.snmp_get_mnesia_key(table, rowIndex) 21 | 22 | @doc """ 23 | TODO: Neet to be completed. 24 | """ 25 | def snmp_get_next_index(table, rowIndex) when is_list(rowIndex), 26 | do: Mnesia.snmp_get_next_index(table, rowIndex) 27 | 28 | @doc """ 29 | TODO: Neet to be completed. 30 | """ 31 | def snmp_get_row(table, rowIndex) when is_list(rowIndex), 32 | do: Mnesia.snmp_get_row(table, rowIndex) 33 | 34 | @doc """ 35 | TODO: Neet to be completed. 36 | """ 37 | def snmp_open_table(table, snmp), do: Mnesia.snmp_open_table(table, snmp) 38 | end 39 | -------------------------------------------------------------------------------- /lib/modules/mnesia_assistant/operation/transaction.ex: -------------------------------------------------------------------------------- 1 | defmodule MnesiaAssistant.Transaction do 2 | @moduledoc """ 3 | In this module, there are functions that are related to Transactions or that are opposite to commands. 4 | """ 5 | alias :mnesia, as: Mnesia 6 | @dialyzer {:nowarn_function, abort: 1} 7 | @doc """ 8 | Termination of a `Mnesia` transaction means that an exception is thrown to an enclosing catch. 9 | Makes the transaction silently return the tuple `{:aborted, reason}`. 10 | Thus, the expression catch `mnesia:abort(x)` (Erlang) does not terminate the transaction. 11 | 12 | ### Example: 13 | 14 | ```elixir 15 | MnesiaAssistant.Transaction.abort(reason) 16 | ``` 17 | """ 18 | def abort(reason), do: Mnesia.abort(reason) 19 | 20 | # ets | async_dirty | sync_dirty | transaction | sync_transaction 21 | # | {transaction, Retries :: integer() >= 0} 22 | # | {sync_transaction, Retries :: integer() >= 0} 23 | @doc """ 24 | ### Erlang document: 25 | 26 | The code that executes inside the activity can consist of a series of table manipulation functions, 27 | which are performed in an `AccessContext`. Currently, the following access contexts are supported: 28 | 29 | - `transaction` Short for {transaction, infinity} 30 | 31 | - `{transaction, Retries}` Calls `mnesia:transaction(Fun, Args, Retries)`. Notice that the result from 32 | Fun is returned if the transaction is successful (atomic), otherwise the function exits with an abort reason. 33 | 34 | - `sync_transaction` Short for {sync_transaction, infinity} 35 | 36 | - `{sync_transaction, Retries}` Calls mnesia:sync_transaction(Fun, Args, Retries). 37 | Notice that the result from Fun is returned if the transaction is successful (atomic), 38 | otherwise the function exits with an abort reason. 39 | 40 | - `async_dirty` Calls `mnesia:async_dirty(Fun, Args)`. 41 | 42 | - `sync_dirty` `Calls mnesia:sync_dirty(Fun, Args)`. 43 | 44 | - `ets` Calls `mnesia:ets(Fun, Args)`. 45 | 46 | This function (`mnesia:activity/4`) differs in an important way from the functions mnesia:transaction, 47 | `mnesia:sync_transaction`, `mnesia:async_dirty`, mnesia:sync_dirty, and `mnesia:ets`. Argument `AccessMod` is the 48 | name of a callback module, which implements the mnesia_access behavior. 49 | Mnesia forwards calls to the following functions: 50 | 51 | - `mnesia:lock/2` (`read_lock_table/1`, `write_lock_table/1`) 52 | - `mnesia:write/3` (`write/1`, `s_write/1`) 53 | - `mnesia:delete/3` (`delete/1`, `s_delete/1`) 54 | - `mnesia:delete_object/3` (`delete_object/1`, `s_delete_object/1`) 55 | - `mnesia:read/3` (`read/1`, `wread/1`) 56 | - `mnesia:match_object/3` (`match_object/1`) 57 | - `mnesia:all_keys/1` 58 | - `mnesia:first/1` 59 | - `mnesia:last/1` 60 | - `mnesia:prev/2` 61 | - `mnesia:next/2` 62 | - `mnesia:index_match_object/4` (`index_match_object/2`) 63 | - `mnesia:index_read/3` 64 | - `mnesia:table_info/2` 65 | 66 | to the corresponding: 67 | 68 | - `AccessMod:lock(ActivityId, Opaque, LockItem, LockKind)` 69 | - `AccessMod:write(ActivityId, Opaque, Tab, Rec, LockKind)` 70 | - `AccessMod:delete(ActivityId, Opaque, Tab, Key, LockKind)` 71 | - `AccessMod:delete_object(ActivityId, Opaque, Tab, RecXS, LockKind)` 72 | - `AccessMod:read(ActivityId, Opaque, Tab, Key, LockKind)` 73 | - `AccessMod:match_object(ActivityId, Opaque, Tab, Pattern, LockKind)` 74 | - `AccessMod:all_keys(ActivityId, Opaque, Tab, LockKind)` 75 | - `AccessMod:first(ActivityId, Opaque, Tab)` 76 | - `AccessMod:last(ActivityId, Opaque, Tab)` 77 | - `AccessMod:prev(ActivityId, Opaque, Tab, Key)` 78 | - `AccessMod:next(ActivityId, Opaque, Tab, Key)` 79 | - `AccessMod:index_match_object(ActivityId, Opaque, Tab, Pattern, Attr, LockKind)` 80 | - `AccessMod:index_read(ActivityId, Opaque, Tab, SecondaryKey, Attr, LockKind)` 81 | - `AccessMod:table_info(ActivityId, Opaque, Tab, InfoItem)` 82 | 83 | ActivityId is a record that represents the identity of the enclosing Mnesia activity. The first field (obtained with element(1, ActivityId)) contains an atom, which can be interpreted as the activity type: ets, `async_dirty`, `sync_dirty`, or tid. tid means that the activity is a transaction. The structure of the rest of the identity record is internal to Mnesia. 84 | 85 | **`Opaque` is an opaque data structure that is internal to Mnesia.** 86 | 87 | > Calls `mnesia:activity(AccessContext, Fun, Args, AccessMod)`, where AccessMod is the default 88 | > access callback module obtained by `mnesia:system_info(access_module)`. Args defaults to `[]` (empty list). 89 | 90 | ### Example: 91 | 92 | ```elixir 93 | MnesiaAssistant.Transaction.activity( 94 | :transaction, 95 | fn -> MnesiaAssistant.Query.read(:table, id) end 96 | ) 97 | ``` 98 | 99 | > The following is a summary of the description: activity can be helpful anytime you need to utilise additional 100 | > alternatives and also change certain extreme functions; nevertheless, if your job is straightforward, extremely 101 | > obvious, and has a direct function, it is preferable to use its own function. 102 | """ 103 | def activity(kind, activity_fun) 104 | when kind in [:ets, :async_dirty, :sync_dirty, :transaction, :sync_transaction], 105 | do: Mnesia.activity(kind, activity_fun) 106 | 107 | def activity({type, retries} = kind, activity_fun) 108 | when type in [:transaction, :sync_transaction] and is_integer(retries), 109 | do: Mnesia.activity(kind, activity_fun) 110 | 111 | @doc """ 112 | Read `activity/2` document. 113 | """ 114 | def activity(kind, activity_fun, args, module) 115 | when kind in [:ets, :async_dirty, :sync_dirty, :transaction, :sync_transaction] and 116 | is_list(args), 117 | do: Mnesia.activity(kind, activity_fun, args, module) 118 | 119 | def activity({type, retries} = kind, activity_fun, args, module) 120 | when type in [:transaction, :sync_transaction] and is_integer(retries) and is_list(args), 121 | do: Mnesia.activity(kind, activity_fun, args, module) 122 | 123 | @doc """ 124 | ### Erlang document: 125 | 126 | Calls the Fun in a context that is not protected by a transaction. The Mnesia function calls performed in the Fun are mapped to the corresponding dirty functions. **This still involves logging, replication, and subscriptions, but there is no locking, local transaction storage, or commit protocols involved**. 127 | 128 | Checkpoint retainers and indexes are updated, but they are updated dirty. As for normal `mnesia:dirty_*` operations, the operations are performed semi-asynchronously. For details, see `mnesia:activity/4` and the User's Guide. 129 | 130 | The Mnesia tables can be manipulated without using transactions. This has some serious disadvantages, but is considerably faster, as the transaction manager is not involved and no locks are set. A dirty operation does, however, guarantee a certain level of consistency, and the dirty operations cannot return garbled records. All dirty operations provide location transparency to the programmer, and a program does not have to be aware of the whereabouts of a certain table to function. 131 | 132 | Notice that it is more than ten times more efficient to read records dirty than within a transaction. 133 | 134 | Depending on the application, it can be a good idea to use the dirty functions for certain operations. Almost all Mnesia functions that can be called within transactions have a dirty equivalent, which is much more efficient. 135 | 136 | However, notice that there is a risk that the database can be left in an inconsistent state if dirty operations are used to update it. Dirty operations are only to be used for performance reasons when it is absolutely necessary. 137 | 138 | > Notice that calling (nesting) `mnesia:[a]sync_dirty` inside a transaction-context inherits the transaction semantics. 139 | 140 | ### Extera: 141 | 142 | :mnesia.async_dirty/2 and :mnesia.async_dirty/1 are two functions in Mnesia that are utilised to perform operations on the database without locking the database and without waiting for the operation to be finished across all replicas. 143 | To put it another way, these methods carry out the specified operation in an asynchronous and "dirty" fashion. 144 | This means that they do not ensure `ACID` qualities, which stands for **atomicity**, **consistency**, **isolation**, and **durability**. As a result, they are more efficient than transactions, but they may also be less secure. `async_dirty` operations are helpful for use cases in which speed is of the utmost importance and the operation does not require strong consistency guarantees. 145 | 146 | ### Example: 147 | 148 | ```elixir 149 | fun = fn -> MnesiaAssistant.Query.write({Person, id, name, age}) end 150 | MnesiaAssistant.Transaction.async_dirty(fun) 151 | ``` 152 | """ 153 | def async_dirty(dirty_fun), do: Mnesia.async_dirty(dirty_fun) 154 | 155 | @doc """ 156 | Read `async_dirty/1` document. 157 | """ 158 | def async_dirty(dirty_fun, args) when is_list(args), do: Mnesia.async_dirty(dirty_fun, args) 159 | 160 | @doc """ 161 | ### Erlang document: 162 | 163 | Calls the Fun in a raw context that is not protected by a transaction. The Mnesia function call is performed in the Fun and performed directly on the local `ETS` tables on the assumption that the local storage type is **`ram_copies`** and the tables are not replicated to other nodes. 164 | Subscriptions are not triggered and checkpoints are not updated, but it is extremely fast. 165 | This function can also be applied to **`disc_copies`** tables if all operations are read only. For details, see `mnesia:activity/4` and the User's Guide. 166 | 167 | Notice that calling (nesting) a `mnesia:ets` inside a **transaction-context** inherits the transaction semantics. 168 | 169 | ### Example: 170 | 171 | ```elixir 172 | MnesiaAssistant.Transaction.ets(fn -> 173 | MnesiaAssistant.Table.all_keys(module) 174 | end) 175 | ``` 176 | 177 | > Remember that `ets/1` and `ets/2` allow for `ETS-lik`e operations but still within the constraints 178 | > and behavior of Mnesia. They can offer performance benefits for certain types of operations 179 | > but use them judiciously and with understanding of their limitations. 180 | """ 181 | def ets(ets_fun), do: Mnesia.ets(ets_fun) 182 | 183 | @doc """ 184 | Read `ets/1` document. 185 | """ 186 | def ets(ets_fun, args) when is_list(args), do: Mnesia.ets(ets_fun, args) 187 | 188 | @doc """ 189 | Not only does it guarantee that the transaction is committed locally, but it also guarantees that 190 | the modifications are properly replicated to all of the other nodes before the function returns. 191 | When using this synchronous replication strategy, it waits for confirmation from all of the nodes, 192 | which guarantees that all replicas are consistent as soon as the transaction is finished. 193 | 194 | ### Erlang docuemnt: 195 | 196 | Waits until data have been committed and logged to disk (if disk is used) on every involved node before it returns, otherwise it behaves as `mnesia:transaction/[1,2,3]`. 197 | 198 | This functionality can be used to avoid that one process overloads a database on another node. 199 | 200 | 201 | - `sync_transaction(Fun)` 202 | - `sync_transaction(Fun, Retries)` 203 | - `sync_transaction(Fun, Args :: [Arg :: term()])` 204 | - `sync_transaction(Fun, Args :: [Arg :: term()], Retries)` 205 | 206 | ### Example: 207 | 208 | ```elixir 209 | alias MnesiaAssistant.Query 210 | 211 | fun = fn -> 212 | Query.write({Person, Query.dirty_last(Person) + 1, "Mishka, 20}) 213 | end 214 | 215 | MnesiaAssistant.Transaction.sync_transaction(fun) 216 | ``` 217 | """ 218 | def sync_transaction(sync_fun) when is_function(sync_fun), do: Mnesia.sync_transaction(sync_fun) 219 | 220 | @doc """ 221 | Read `sync_transaction/1` document. 222 | """ 223 | def sync_transaction(sync_fun, retries) when is_function(sync_fun) and is_integer(retries), 224 | do: Mnesia.sync_transaction(sync_fun, retries) 225 | 226 | def sync_transaction(sync_fun, args) when is_function(sync_fun) and is_list(args), 227 | do: Mnesia.sync_transaction(sync_fun, args) 228 | 229 | @doc """ 230 | Read `sync_transaction/1` document. 231 | """ 232 | def sync_transaction(sync_fun, args, retries) 233 | when is_function(sync_fun) and is_list(args) and is_integer(retries), 234 | do: Mnesia.sync_transaction(sync_fun, args, retries) 235 | 236 | @doc """ 237 | In contrast to the `sync_transaction/1` function, it guarantees that the transaction is not only committed locally, 238 | but that the modifications are also successfully replicated to all of the other nodes before the function returns. 239 | When using this synchronous replication strategy, it waits for confirmation from all of the nodes, which guarantees that all replicas are consistent as soon as the transaction is finished. 240 | 241 | ### Erlang document 242 | 243 | Executes the functional object Fun with arguments `Args` as a transaction. 244 | 245 | The code that executes inside the transaction can consist of a series of table manipulation functions. 246 | If something goes wrong inside the transaction as a result of a user error or a certain 247 | table not being available, the entire transaction is terminated and the function `transaction/1` returns the tuple `{aborted, Reason}`. 248 | 249 | If all is going well, {atomic, ResultOfFun} is returned, where ResultOfFun is the value 250 | of the last expression in Fun. 251 | 252 | A function that adds a family to the database can be written as follows if there is a structure `{family, Father, Mother, ChildrenList}`: 253 | 254 | ```erlang 255 | add_family({family, F, M, Children}) -> 256 | ChildOids = lists:map(fun oid/1, Children), 257 | Trans = fun() -> 258 | mnesia:write(F#person{children = ChildOids}), 259 | mnesia:write(M#person{children = ChildOids}), 260 | Write = fun(Child) -> mnesia:write(Child) end, 261 | lists:foreach(Write, Children) 262 | end, 263 | mnesia:transaction(Trans). 264 | 265 | oid(Rec) -> {element(1, Rec), element(2, Rec)}. 266 | ``` 267 | This code adds a set of people to the database. Running this code within one transaction ensures 268 | that either the whole family is added to the database, or the whole transaction terminates. 269 | For example, if the last child is badly formatted, or the executing process terminates 270 | because of an `'EXIT'` signal while executing the family code, the transaction terminates. 271 | Thus, the situation where half a family is added can never occur. 272 | 273 | It is also useful to update the database within a transaction if several processes concurrently 274 | update the same records. For example, the function raise(Name, Amount), which adds Amount 275 | to the salary field of a person, is to be implemented as follows: 276 | 277 | ```erlang 278 | raise(Name, Amount) -> 279 | mnesia:transaction(fun() -> 280 | case mnesia:wread({person, Name}) of 281 | [P] -> 282 | Salary = Amount + P#person.salary, 283 | P2 = P#person{salary = Salary}, 284 | mnesia:write(P2); 285 | _ -> 286 | mnesia:abort("No such person") 287 | end 288 | end). 289 | ``` 290 | 291 | When this function executes within a transaction, several processes running on different nodes 292 | can concurrently execute the function `raise/2` without interfering with each other. 293 | 294 | 295 | Since Mnesia detects deadlocks, a transaction can be restarted any number of times and 296 | therefore the `Fun` shall not have any side effects such as waiting for specific messages. 297 | This function attempts a restart as many times as specified in Retries. 298 | Retries must be an integer greater than `0` or the atom infinity, default is infinity. 299 | Mnesia uses exit exceptions to signal that a transaction needs to be restarted, thus a Fun must not catch exit exceptions with reason `{aborted, term()}`. 300 | 301 | - `transaction(Fun)` 302 | - `transaction(Fun, Retries)` 303 | - `transaction(Fun, Args :: [Arg :: term()])` 304 | - `transaction(Fun, Args :: [Arg :: term()], Retries)` 305 | 306 | 307 | ### Example: 308 | 309 | ```elixir 310 | trans = fn -> 311 | MnesiaAssistant.Query.write({Person, 10, "mishka"}) 312 | MnesiaAssistant.Query.write({Person, 11, "life"}) 313 | end 314 | 315 | MnesiaAssistant.Transaction.transaction(trans) 316 | ``` 317 | """ 318 | def transaction(transaction_fn) when is_function(transaction_fn), 319 | do: Mnesia.transaction(transaction_fn) 320 | 321 | @doc """ 322 | Read `transaction/1` document. 323 | """ 324 | def transaction(transaction_fn, retries) 325 | when is_function(transaction_fn) and (is_integer(retries) or retries == :infinity), 326 | do: Mnesia.transaction(transaction_fn, retries) 327 | 328 | def transaction(transaction_fn, args) when is_function(transaction_fn) and is_list(args), 329 | do: Mnesia.transaction(transaction_fn, args) 330 | 331 | @doc """ 332 | Read `transaction/1` document. 333 | """ 334 | def transaction(transaction_fn, args, retries) 335 | when is_function(transaction_fn) and is_list(args) and 336 | (is_integer(retries) or retries == :infinity), 337 | do: Mnesia.transaction(transaction_fn, args, retries) 338 | 339 | def transaction_error(reason, module, type, field, action) do 340 | {:error, error, msg} = MnesiaAssistant.Error.error_description({:aborted, reason}, module) 341 | 342 | message = 343 | "Unfortunately, there is a problem in #{type} data in the database. #{inspect(msg)}" 344 | 345 | {:error, [%{message: message, field: field, action: action, source: error}]} 346 | end 347 | end 348 | -------------------------------------------------------------------------------- /lib/modules/mnesia_assistant/pagination.ex: -------------------------------------------------------------------------------- 1 | defmodule MnesiaAssistant.Pagination do 2 | alias MnesiaAssistant.{Transaction, Query} 3 | import MnesiaAssistant, only: [eg: 1, er: 1, tuple_to_map: 4] 4 | 5 | def infinite_scroll({_record, _cont} = output, _table, keys, strc, _limit) do 6 | infinite_scroll(output, keys, strc) 7 | end 8 | 9 | def infinite_scroll(table, keys, strc, limit, spec) do 10 | match_pattern = 11 | if is_nil(spec) do 12 | fields = 13 | ([table] ++ Enum.map(1..length(keys), fn _x -> :_ end)) 14 | |> List.to_tuple() 15 | 16 | [{fields, [], er(:all)}] 17 | else 18 | spec 19 | end 20 | 21 | Transaction.activity(:async_dirty, fn -> Query.select(table, match_pattern, limit, :read) end) 22 | |> tuple_to_map(keys, strc, []) 23 | |> case do 24 | [] -> [] 25 | data -> data 26 | end 27 | end 28 | 29 | def infinite_scroll({_record, cont}, keys, strc) do 30 | Transaction.activity(:async_dirty, fn -> Query.select(cont) end) 31 | |> tuple_to_map(keys, strc, []) 32 | |> case do 33 | [] -> [] 34 | data -> data 35 | end 36 | end 37 | 38 | def infinite_scroll(table, keys, strc, limit, page, spec) when page >= 1 do 39 | match_pattern = 40 | if is_nil(spec) do 41 | fields = 42 | ([table] ++ Enum.map(1..length(keys), fn _x -> :_ end)) 43 | |> List.to_tuple() 44 | 45 | [{fields, [], er(:all)}] 46 | else 47 | spec 48 | end 49 | 50 | Transaction.activity(:async_dirty, fn -> Query.select(table, match_pattern, limit, :read) end) 51 | |> tuple_to_map(keys, strc, []) 52 | |> case do 53 | [] -> [] 54 | data when data != [] and page == 1 -> elem(data, 0) 55 | data -> go_next_page(page, data, keys, strc) 56 | end 57 | end 58 | 59 | defp go_next_page(page, {record, cont}, keys, strc) do 60 | Enum.reduce_while(1..(page - 1), {record, cont}, fn item, acc -> 61 | Transaction.activity(:async_dirty, fn -> Query.select(elem(acc, 1)) end) 62 | |> tuple_to_map(keys, strc, []) 63 | |> case do 64 | [] -> 65 | {:halt, []} 66 | 67 | {new_record, new_cont} -> 68 | if item + 1 == page, do: {:halt, new_record}, else: {:cont, {new_record, new_cont}} 69 | end 70 | end) 71 | end 72 | 73 | # mnesia:transaction(fun() -> mnesia:select(tab, 74 | # [{{tab,'$1','_','_','_','_','_','_','_','_','_','_','_','_','_'}, 75 | # [{'andalso', {'>', '$1', 20}, {'<', '$1', 30}}], ['$_']}], 11, read) 76 | # end). 77 | def numerical(table, keys, strc, start: start, last: last, at: at) do 78 | fields = 79 | ([table] ++ 80 | Enum.map(1..length(keys), fn x -> if(x === at, do: :"$1", else: :_) end)) 81 | |> List.to_tuple() 82 | 83 | conds = [{eg(:and), {eg(:>=), :"$1", start}, {eg(:<=), :"$1", last}}] 84 | spec = [{fields, conds, er(:all)}] 85 | run_numerical_query(table, spec, keys, strc) 86 | end 87 | 88 | def numerical(table, keys, strc, page, limit, at) when page > 0 and limit > 0 do 89 | start = page * limit - limit 90 | 91 | fields = 92 | ([table] ++ 93 | Enum.map(1..length(keys), fn x -> if(x === at, do: :"$1", else: :_) end)) 94 | |> List.to_tuple() 95 | 96 | conds = [{eg(:and), {eg(:>), :"$1", start}, {eg(:<=), :"$1", page * limit}}] 97 | spec = [{fields, conds, er(:all)}] 98 | run_numerical_query(table, spec, keys, strc) 99 | end 100 | 101 | defp run_numerical_query(table, spec, keys, strc) do 102 | Transaction.transaction(fn -> Query.select(table, spec, :read) end) 103 | |> case do 104 | {:atomic, res} -> 105 | tuple_to_map(res, keys, strc, []) 106 | 107 | {:aborted, reason} -> 108 | Transaction.transaction_error(reason, __MODULE__, "listing", :global, :database) 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /lib/modules/permission_access.ex: -------------------------------------------------------------------------------- 1 | defmodule PermissionAccess do 2 | @moduledoc """ 3 | Consider the scenario in which you are responsible for maintaining each user's access 4 | level in the database related to users. In addition, each router in your controller needs 5 | to be free for one access while preventing other things from accessing it. To achieve 6 | this goal, the PermissionAccess module provides assistance in implementing a Unix-like mode 7 | in the most straightforward manner feasible. 8 | This module was written with the contribution of Mr. Toomaj Boloorian, 9 | who can be found at the following GitHub address: https://github.com/toomaj 10 | and and Shahryar Tavakkoli: https://github.com/shahryarjb 11 | """ 12 | @separator ":" 13 | 14 | @type action() :: String.t() 15 | @type user_permissions() :: list(String.t()) 16 | 17 | @doc """ 18 | ### Allow access to the user 19 | 20 | Access in this section is referred to as an action, and in addition, there are two sections included. 21 | 22 | The first portion may grant access to an entire department or to an entire role, whereas the second part 23 | may be delegated to a specific part. But this explanation is subject to alter depending on the strategy 24 | you choose. Because of this, pay close attention to the instances that follow. 25 | 26 | Within this section, the user has access to all portions of your program thanks to the wildcard permissions that have been granted to him. 27 | 28 | **Note**: You have the option of assigning a star `*` rating or writing it as `*:*`. 29 | 30 | ```elixir 31 | @admin_router %{ 32 | # admin router 33 | "AdminDashboardLive" => "admin:*", 34 | "AdminBlogPostsLive" => "admin:edit", 35 | "AdminBlogPostLive" => "admin:edit", 36 | "AdminBlogCategoriesLive" => "admin:edit", 37 | "AdminBlogCategoryLive" => "admin:edit", 38 | "AdminBookmarksLive" => "*", 39 | "AdminSubscriptionsLive" => "*", 40 | "AdminSubscriptionLive" => "*", 41 | "AdminCommentsLive" => "admin:edit", 42 | "AdminCommentLive" => "admin:edit", 43 | "AdminUsersLive" => "*", 44 | "AdminUserLive" => "*", 45 | "AdminLogsLive" => "*", 46 | "AdminSeoLive" => "*", 47 | "AdminBlogPostAuthorsLive" => "admin:edit", 48 | "AdminBlogNotifLive" => "*", 49 | "AdminBlogNotifsLive" => "admin:view" 50 | } 51 | 52 | user_actions = [%{value: "*"}] 53 | PermissionAccess.permittes?(user_actions, @admin_router[item]) 54 | # This should be true, and the user has access. 55 | 56 | user_actions = [%{value: "*:edit"}] 57 | PermissionAccess.permittes?(user_actions, @admin_router["AdminBlogNotifsLive"]) 58 | # This should be false, and the user has no access. 59 | 60 | user_actions = [%{value: "*:edit"}, %{value: "admin:view"}] 61 | PermissionAccess.permittes?(user_actions, @admin_router["AdminBlogNotifsLive"]) 62 | # This should be true, and the user has access. 63 | ``` 64 | """ 65 | @spec permittes?(user_permissions(), action()) :: boolean 66 | def permittes?(user_permissions, action) do 67 | Enum.any?( 68 | user_permissions, 69 | fn %{value: permission} -> 70 | is_permitted?(action: action, permission: permission) 71 | end 72 | ) 73 | end 74 | 75 | @doc """ 76 | This section is identical to the function known as `permittes?/2`, with the exception that 77 | it examines only a single user access rather than a list of accesses. 78 | 79 | ```elixir 80 | PermissionAccess.is_permitted?(action: "*", @admin_router["AdminBlogNotifsLive"]) 81 | ``` 82 | """ 83 | @spec is_permitted?([{:action, action()} | {:permission, binary}]) :: boolean 84 | def is_permitted?(action: action, permission: permission) do 85 | permission_chunks = String.split(permission, @separator) 86 | 87 | String.split(action, @separator, parts: length(permission_chunks)) 88 | |> check_permission(permission_chunks) 89 | end 90 | 91 | defp check_permission(action_chunks, permission_chunks) 92 | when length(permission_chunks) != length(action_chunks), 93 | do: false 94 | 95 | defp check_permission(action_chunks, permission_chunks) do 96 | Enum.zip(permission_chunks, action_chunks) 97 | |> Enum.find(fn {left, right} -> 98 | cond do 99 | left == "*" -> false 100 | left != right -> true 101 | true -> false 102 | end 103 | end) 104 | |> is_nil() 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /lib/modules/queue_assistant.ex: -------------------------------------------------------------------------------- 1 | defmodule QueueAssistant do 2 | @moduledoc """ 3 | This is a simple wrapper for the Erlang queue, in which the order of some entries 4 | has also been changed 5 | 6 | - **Based on:** https://www.erlang.org/doc/man/queue 7 | """ 8 | 9 | @type queue_type :: :queue.queue(any()) 10 | 11 | @doc """ 12 | Returns true if all elements in enumerable are truthy. It is like `Enum.all?/1`. 13 | 14 | ### Example: 15 | ```elixir 16 | new() 17 | |> insert(1) 18 | |> all?(fn x -> x >= 1 end) 19 | ``` 20 | """ 21 | @spec all?((any() -> boolean()), queue_type()) :: boolean() 22 | def all?(fun, queue) do 23 | :queue.all(fun, queue) 24 | end 25 | 26 | @doc """ 27 | Returns true if at least one element in enumerable is truthy. 28 | 29 | ### Example: 30 | ```elixir 31 | new() 32 | |> insert(1) 33 | |> insert(2) 34 | |> any?(fn x -> x >= 2 end) 35 | ``` 36 | """ 37 | @spec any?(queue_type(), (any() -> boolean())) :: boolean() 38 | def any?(queue, fun) do 39 | :queue.any(fun, queue) 40 | end 41 | 42 | @doc """ 43 | Returns a copy of `q1` where the first item matching `item` is deleted, if there is such an item. 44 | 45 | ### Example: 46 | ```elixir 47 | queue = from_list([1, 2, 3, 4, 5]) 48 | queue1 = queue |> delete(3) 49 | member(queue1, 3) 50 | # false 51 | ``` 52 | """ 53 | @spec delete(queue_type(), any()) :: queue_type() 54 | def delete(queue, item) do 55 | :queue.delete(item, queue) 56 | end 57 | 58 | @doc """ 59 | Returns a copy of `q1` where the last item matching `item` is deleted, if there is such an item. 60 | 61 | ### Example: 62 | ```elixir 63 | queue = from_list([1, 2, 3, 4, 3, 5]) 64 | queue1 = delete_r(queue, 3) 65 | to_list(queue1). 66 | [1,2,3,4,5] 67 | ``` 68 | """ 69 | @spec delete_r(queue_type(), any()) :: queue_type() 70 | def delete_r(queue, item) do 71 | :queue.delete_r(item, queue) 72 | end 73 | 74 | @doc """ 75 | Returns a copy of `q1` where the first item for which `fn` returns true is deleted, 76 | if there is such an item. 77 | 78 | ### Example: 79 | ```elixir 80 | queue = from_list([100,1, 2, 3, 4, 5]) 81 | queue1 = delete_with(queue, fn x -> x > 0) 82 | to_list(queue1) 83 | [1,2,3,4,5] 84 | ``` 85 | 86 | If we make it abstract in Elixir, you can do it with a list like this: 87 | 88 | ```elixir 89 | list = [1, 2, 3, 4, 5] 90 | {before, [h | t]} = Enum.split_with(list, fn x -> x < 4 end) 91 | new_list = before ++ t 92 | ``` 93 | """ 94 | @spec delete_with(queue_type(), (any() -> boolean())) :: queue_type() 95 | def delete_with(queue, fun) do 96 | :queue.delete_with(fun, queue) 97 | end 98 | 99 | @doc """ 100 | Returns a copy of `q1` where the last item for which `fn` returns true is deleted, 101 | if there is such an item. 102 | 103 | ### Example: 104 | ```elixir 105 | queue = from_list([100,1, 4, 2, 3, 4, 5]) 106 | queue1 = delete_with_r(queue, fn x -> x == 4) 107 | to_list(queue1) 108 | [100,1, 4, 2, 3, 5] 109 | ``` 110 | """ 111 | @spec delete_with_r(queue_type(), (any() -> boolean())) :: queue_type() 112 | def delete_with_r(queue, fun) do 113 | :queue.delete_with_r(fun, queue) 114 | end 115 | 116 | @doc """ 117 | Filters the enumerable, i.e. returns only those elements for which fun returns a truthy value. 118 | It is like `Enum.filter/2`. 119 | 120 | ### Example: 121 | ```elixir 122 | queue = from_list([1, 2, 3, 4, 5]) 123 | queue1 = filter(queue, fn x -> x > 2 end) 124 | to_list(queue1) 125 | # [3, 4, 5] 126 | ``` 127 | 128 | ### From Erlang docs: 129 | 130 | > So, `Fun(Item)` returning `[Item]` is thereby semantically equivalent to returning true, 131 | > just as returning `[]` is semantically equivalent to returning false. 132 | > 133 | > But returning a list builds more garbage than returning an atom. 134 | 135 | ### Example: 136 | ```elixir 137 | queue = from_list([1, 2, 3, 4, 5]) 138 | # {[5, 4, 3], [1, 2]} 139 | 140 | queue1 = filter(queue, fn x -> [x, x+1] end) 141 | # {[6, 5, 5, 4, 4, 3], [1, 2, 2, 3]} 142 | 143 | to_list(queue1) 144 | # [1, 2, 2, 3, 3, 4, 4, 5, 5, 6] 145 | ``` 146 | """ 147 | @spec filter(queue_type(), (any() -> boolean() | list())) :: queue_type() 148 | def filter(queue, fun) do 149 | :queue.filter(fun, queue) 150 | end 151 | 152 | @doc """ 153 | ### From Erlang doc: 154 | 155 | Returns a queue `q2` that is the result of calling `Fun(Item)` on all items in `q1`. 156 | 157 | If `Fun(Item)` returns true, Item is copied to the result queue. If it returns **false**, 158 | Item is not copied. 159 | 160 | If it returns `{true, NewItem}`, the queue element at this position is replaced with `NewItem` 161 | in the result queue. 162 | 163 | ### Example: 164 | ```elixir 165 | queue = from_list([1, 2, 3, 4, 5]) 166 | queue1 = filtermap(queue, fn x -> x > 2 end) 167 | to_list(queue1) 168 | # [3, 4, 5] 169 | ``` 170 | """ 171 | @spec filtermap(queue_type(), (any() -> boolean() | {true, any()})) :: 172 | queue_type() 173 | def filtermap(queue, fun) do 174 | :queue.filtermap(fun, queue) 175 | end 176 | 177 | @doc """ 178 | Invokes fun for each element in the enumerable with the accumulator. It is like `Enum.reduce/3`. 179 | 180 | ### From Erlang docs: 181 | 182 | Calls Fun(Item, AccIn) on successive items Item of Queue, starting with `AccIn == Acc`0. 183 | The queue is traversed in queue order, that is, from front to rear. 184 | 185 | `Fun/2` must return a new accumulator, which is passed to the next call. 186 | The function returns the final value of the accumulator. `Acc0` is returned if the queue is empty. 187 | 188 | ### Example: 189 | ```elixir 190 | queue = from_list([1, 2, 3, 4, 5]) 191 | 192 | 1> fold(queue, 0, fn item, acc -> item + acc end) 193 | # 15 194 | 195 | 2> fold(queue, 0, fn item, acc -> item * acc end) 196 | # 120 197 | ``` 198 | """ 199 | @spec fold(queue_type(), any(), (any(), any() -> any())) :: any() 200 | def fold(queue, acc, fun) do 201 | :queue.fold(fun, acc, queue) 202 | end 203 | 204 | @doc """ 205 | For see more information, check `fold/3` 206 | """ 207 | @spec reduce(queue_type(), any(), (any(), any() -> any())) :: any() 208 | def reduce(queue, acc, fun), do: fold(queue, acc, fun) 209 | 210 | @doc """ 211 | Returns a queue containing the items in the same order; the head item of 212 | the list becomes the front item of the queue. 213 | 214 | ### Example: 215 | ```elixir 216 | from_list([1, 2, 3, 4, 5]) 217 | # {[5, 4, 3], [1, 2]} 218 | ``` 219 | """ 220 | @spec from_list(list()) :: queue_type() 221 | def from_list(items) do 222 | :queue.from_list(items) 223 | end 224 | 225 | @doc """ 226 | Inserts Item at the rear of queue `q1`. Returns the resulting queue `q2`. 227 | 228 | ### Example: 229 | ```elixir 230 | queue = from_list([1, 2, 3, 4, 5]) 231 | queue1 = insert(queue, 100) 232 | to_list(queue1) 233 | # [1, 2, 3, 4, 5, 100] 234 | ``` 235 | """ 236 | @spec insert(queue_type(), any()) :: queue_type() 237 | def insert(queue, item) do 238 | :queue.in(item, queue) 239 | end 240 | 241 | @doc """ 242 | Inserts Item at the front of queue `q1`. Returns the resulting queue `q2`. 243 | 244 | ### Example: 245 | ```elixir 246 | queue = from_list([1, 2, 3, 4, 5]) 247 | queue1 = insert_r(queue, 100) 248 | to_list(queue1) 249 | # [100, 1, 2, 3, 4, 5] 250 | ``` 251 | """ 252 | @spec insert_r(queue_type(), any()) :: queue_type() 253 | def insert_r(queue, item) do 254 | :queue.in_r(item, queue) 255 | end 256 | 257 | @doc """ 258 | For see more information, check `insert_r/2` 259 | """ 260 | @spec in_r(queue_type(), any()) :: queue_type() 261 | def in_r(queue, item), do: insert_r(queue, item) 262 | 263 | @doc """ 264 | Tests if a queue is empty and returns true if so, otherwise false. 265 | 266 | ### Example: 267 | ```elixir 268 | queue = from_list([1, 2, 3, 4, 5]) 269 | is_empty?(queue) 270 | # false 271 | ``` 272 | """ 273 | @spec is_empty?(queue_type() | nil) :: boolean() 274 | def is_empty?(nil), do: true 275 | 276 | def is_empty?(queue) do 277 | :queue.is_empty(queue) 278 | end 279 | 280 | @spec empty?(queue_type()) :: boolean() 281 | @doc """ 282 | For see more information, check `is_empty?/1` 283 | """ 284 | @spec empty?(queue_type() | nil) :: boolean() 285 | def empty?(queue), do: is_empty?(queue) 286 | 287 | @doc """ 288 | Tests if an entry is queue and returns true if so, otherwise false. 289 | 290 | ### Example: 291 | ```elixir 292 | queue = from_list([1, 2, 3, 4, 5]) 293 | is_queue?(queue) 294 | # true 295 | ``` 296 | """ 297 | @spec is_queue?(queue_type()) :: boolean() 298 | def is_queue?(queue) do 299 | :queue.is_queue(queue) 300 | end 301 | 302 | @doc """ 303 | For see more information, check `is_queue?/1` 304 | """ 305 | @spec queue?(queue_type()) :: boolean() 306 | def queue?(queue), do: is_queue?(queue) 307 | 308 | @doc """ 309 | Returns a queue `q3` that is the result of joining `q1` and `q2` with `q1` in front of `q2`. 310 | 311 | ### Example: 312 | ```elixir 313 | queue = from_list([1, 2]) 314 | queue1 = from_list([3, 4]) 315 | join(queue, queue1) 316 | # [1, 2, 3, 4] 317 | ``` 318 | """ 319 | @spec join(queue_type(), queue_type()) :: queue_type() 320 | def join(queue, queue1) do 321 | :queue.join(queue, queue1) 322 | end 323 | 324 | @doc """ 325 | Calculates and returns the length of a queue. 326 | 327 | ### Example: 328 | ```elixir 329 | queue = from_list([1, 2]) 330 | len(queue) 331 | # 2 332 | ``` 333 | """ 334 | @spec len(queue_type()) :: non_neg_integer() 335 | def len(queue) do 336 | :queue.len(queue) 337 | end 338 | 339 | @doc """ 340 | For see more information, check `len/1` 341 | """ 342 | @spec count(queue_type()) :: non_neg_integer() 343 | def count(queue), do: len(queue) 344 | 345 | @doc """ 346 | Checks if element exists within the queue. 347 | 348 | ### Example: 349 | ```elixir 350 | queue = from_list([1, 2]) 351 | member?(queue, 2) 352 | # true 353 | ``` 354 | """ 355 | @spec member?(queue_type(), any()) :: boolean() 356 | def member?(queue, item) do 357 | :queue.member(item, queue) 358 | end 359 | 360 | @doc """ 361 | Returns an empty queue. 362 | ### Example: 363 | ```elixir 364 | new() 365 | # {[], []} 366 | ``` 367 | """ 368 | @spec new() :: queue_type() 369 | def new() do 370 | :queue.new() 371 | end 372 | 373 | @doc """ 374 | Removes the item at the front of queue `q1`. Returns tuple `{{:value, item}, q2}`, 375 | where Item is the item removed and `q2` is the resulting queue. 376 | If `q1` is empty, tuple `{empty, q1}` is returned. 377 | 378 | ### Example: 379 | ```elixir 380 | queue = from_list([1, 2, 3, 4, 5]) 381 | # {[5, 4, 3],[1, 2]} 382 | 383 | {{:value, 1}, new_queue} = out(queue) 384 | # {{:value, 1}, {[5, 4, 3], [2]}} 385 | 386 | to_list(new_queue) 387 | # [2, 3, 4, 5] 388 | ``` 389 | """ 390 | @dialyzer {:nowarn_function, out: 1} 391 | @spec out(queue_type()) :: {:empty, {[], []}} | {{:value, any()}, queue_type()} 392 | def out(queue) do 393 | :queue.out(queue) 394 | end 395 | 396 | @doc """ 397 | Removes the item at the rear of queue `q1`. Returns tuple `{{:value, item}, q2}`, 398 | where Item is the item removed and `q2` is the resulting queue. 399 | If `q1` is empty, tuple `{empty, q1}` is returned. 400 | 401 | ### Example: 402 | ```elixir 403 | queue = from_list([1, 2, 3, 4, 5]) 404 | # {[5,4,3], [1, 2]} 405 | 406 | {{:value, 5}, new_queue} = out_r(queue) 407 | # {{:value, 5}, {[5, 4, 3], [2, 1]}} 408 | 409 | to_list(new_queue) 410 | # [1, 2, 3, 4] 411 | ``` 412 | """ 413 | @spec out_r(queue_type()) :: {:empty | {:value, any()}, queue_type()} 414 | def out_r(queue) do 415 | :queue.out_r(queue) 416 | end 417 | 418 | @doc """ 419 | Returns a queue `q2` containing the items of `q1` in the reverse order. 420 | 421 | ### Example: 422 | ```elixir 423 | queue = from_list([1, 2, 3, 4, 5]) 424 | reverse(queue) 425 | # {[1, 2], [5, 4, 3]} 426 | ``` 427 | """ 428 | @spec reverse(queue_type()) :: queue_type() 429 | def reverse(queue) do 430 | :queue.reverse(queue) 431 | end 432 | 433 | @doc """ 434 | Splits `q1` in two. The `n` front items are put in `q2` and the rest in `q3`. 435 | 436 | ### Example: 437 | ```elixir 438 | queue = from_list([1, 2, 3, 4, 5]) 439 | split(queue, 2) 440 | # {{[2], [1]}, {[5], [3, 4]}} 441 | ``` 442 | """ 443 | @spec split(queue_type(), non_neg_integer()) :: 444 | {queue_type(), queue_type()} 445 | def split(queue, at) do 446 | :queue.split(at, queue) 447 | end 448 | 449 | @doc """ 450 | Returns a list of the items in the queue in the same order; the front item of 451 | the queue becomes the head of the list. 452 | 453 | ### Example: 454 | ```elixir 455 | queue = from_list([1, 2, 3, 4, 5]) 456 | # {[5, 4, 3], [1, 2]} 457 | 458 | to_list(queue) 459 | # [1, 2, 3, 4, 5] 460 | ``` 461 | """ 462 | @spec to_list(queue_type()) :: list() 463 | def to_list(queue) do 464 | :queue.to_list(queue) 465 | end 466 | 467 | # Extended APIs 468 | 469 | @doc """ 470 | Returns a queue `q2` that is the result of removing the front item from `q1`. 471 | 472 | ### Example: 473 | ```elixir 474 | queue = from_list([1, 2, 3, 4, 5]) 475 | queue1 = drop(queue) 476 | to_list(queue1) 477 | # [2,3,4,5] 478 | ``` 479 | 480 | > **Fails with reason empty if `q1` is empty.** You must check if it is empty before dropping it. 481 | 482 | ### Error output: 483 | 484 | ``` 485 | ** (ErlangError) Erlang error: :empty 486 | (stdlib 5.2.1) queue.erl:252: :queue.drop({[], []}) 487 | iex:55: (file) 488 | ``` 489 | """ 490 | @spec drop(queue_type()) :: queue_type() 491 | def drop(queue) do 492 | :queue.drop(queue) 493 | end 494 | 495 | @doc """ 496 | Returns a queue `q2` that is the result of removing the rear item from `q1`. 497 | 498 | ### Example: 499 | ```elixir 500 | queue = from_list([1, 2, 3, 4, 5]) 501 | queue1 = drop_r(queue) 502 | to_list(queue1) 503 | # [1, 2, 3, 4] 504 | ``` 505 | 506 | > **Fails with reason empty if `q1` is empty.** You must check if it is empty before dropping it. 507 | 508 | ### Error output: 509 | 510 | ``` 511 | ** (ErlangError) Erlang error: :empty 512 | (stdlib 5.2.1) queue.erl:270: :queue.drop_r({[], []}) 513 | iex:58: (file) 514 | ``` 515 | """ 516 | @spec drop_r(queue_type()) :: queue_type() 517 | def drop_r(queue) do 518 | :queue.drop_r(queue) 519 | end 520 | 521 | @doc """ 522 | Returns Item at the front of a queue. 523 | 524 | ### Example: 525 | ```elixir 526 | queue = from_list([1, 2, 3, 4, 5]) 527 | 1 == get(queue) 528 | ``` 529 | 530 | > **Fails with reason empty if `queue` is empty.** You must check if it is empty before getting it. 531 | 532 | ### Error output: 533 | 534 | ``` 535 | ** (ErlangError) Erlang error: :empty 536 | (stdlib 5.2.1) queue.erl:188: :queue.get({[], []}) 537 | iex:58: (file) 538 | ``` 539 | """ 540 | @spec get(queue_type()) :: any() 541 | def get(queue) do 542 | :queue.get(queue) 543 | end 544 | 545 | @doc """ 546 | Returns Item at the rear of a queue. 547 | 548 | ### Example: 549 | ```elixir 550 | queue = from_list([1, 2, 3, 4, 5]) 551 | 5 == get_r(queue) 552 | ``` 553 | 554 | > **Fails with reason empty if `queue` is empty.** You must check if it is empty before getting it. 555 | 556 | ### Error output: 557 | 558 | ``` 559 | ** (ErlangError) Erlang error: :empty 560 | (stdlib 5.2.1) queue.erl:207: :queue.get_r({[], []}) 561 | iex:58: (file) 562 | ``` 563 | """ 564 | @spec get_r(queue_type()) :: any() 565 | def get_r(queue) do 566 | :queue.get_r(queue) 567 | end 568 | 569 | @doc """ 570 | Returns tuple `{:value, item}`, where Item is the front item of a queue, 571 | or empty if a queue is `:empty`. 572 | 573 | ### Example: 574 | ```elixir 575 | peek(new()) 576 | # :empty 577 | 578 | queue = from_list([1, 2 ,3 ,4 ,5]) 579 | # {[5, 4, 3], [1, 2]} 580 | 581 | peek(queue) 582 | # {:value, 1} 583 | ``` 584 | """ 585 | @spec peek(queue_type()) :: :empty | {:value, any()} 586 | def peek(queue) do 587 | :queue.peek(queue) 588 | end 589 | 590 | @doc """ 591 | Returns tuple `{:value, item}`, where Item is the rear item of a queue, 592 | or empty if a queue is `:empty`. 593 | 594 | ### Example: 595 | ```elixir 596 | peek_r(new()) 597 | # :empty 598 | 599 | queue = from_list([1, 2 ,3 ,4 ,5]) 600 | # {[5, 4, 3], [1, 2]} 601 | 602 | peek_r(queue) 603 | # {:value, 5} 604 | ``` 605 | """ 606 | @spec peek_r(queue_type()) :: :empty | {:value, any()} 607 | def peek_r(queue) do 608 | :queue.peek_r(queue) 609 | end 610 | 611 | @doc """ 612 | Please see `join/2` and `to_list/1`. 613 | """ 614 | @spec join_to_list(queue_type(), queue_type()) :: list(any()) 615 | def join_to_list(queue, queue1) do 616 | :queue.join(queue, queue1) 617 | |> :queue.to_list() 618 | end 619 | 620 | @doc """ 621 | Please see `join/2` and `to_list/1`. 622 | """ 623 | @spec join_to_list(list(any()), list(any())) :: queue_type() 624 | def list_to_join(queue, queue1) do 625 | :queue.join(:queue.from_list(queue), :queue.from_list(queue1)) 626 | end 627 | 628 | # The "Okasaki API" is inspired by "Purely Functional Data Structures" by Chris Okasaki. 629 | # It regards queues as lists. This API is by many regarded as strange and avoidable. 630 | # For example, many reverse operations have lexically reversed names, some with more 631 | # readable but perhaps less understandable aliases. 632 | # 633 | # Do not use these functions 634 | 635 | @doc """ 636 | Inserts Item at the head of queue `q1`. Returns the new queue `q2`. 637 | 638 | ### Example: 639 | ```elixir 640 | queue = cons(from_list([1, 2, 3]), 0) 641 | to_list queue 642 | # [0, 1, 2, 3] 643 | ``` 644 | """ 645 | @spec cons(queue_type(), any()) :: queue_type() 646 | def cons(queue, item) do 647 | :queue.cons(item, queue) 648 | end 649 | 650 | @doc """ 651 | Returns the tail item of a queue. 652 | 653 | ### Example: 654 | ```elixir 655 | queue = daeh(from_list([1, 2, 3])) 656 | # 3 657 | ``` 658 | 659 | > **Fails with reason empty if `queue` is empty.** You must check if it is empty before tailing it. 660 | 661 | ### Error output: 662 | 663 | ``` 664 | ** (ErlangError) Erlang error: :empty 665 | (stdlib 5.2.1) queue.erl:207: :queue.get_r({[], []}) 666 | iex:67: (file) 667 | ``` 668 | """ 669 | @spec daeh(queue_type()) :: any() 670 | def daeh(queue) do 671 | :queue.daeh(queue) 672 | end 673 | 674 | @doc """ 675 | Returns the head item of a queue. 676 | 677 | ### Example: 678 | ```elixir 679 | queue = head(from_list([1, 2, 3])) 680 | # 1 681 | ``` 682 | 683 | > **Fails with reason empty if `queue` is empty.** You must check if it is empty before heading it. 684 | 685 | ### Error output: 686 | 687 | ``` 688 | ** (ErlangError) Erlang error: :empty 689 | (stdlib 5.2.1) queue.erl:662: :queue.head({[], []}) 690 | iex:67: (file) 691 | ``` 692 | """ 693 | @spec head(queue_type()) :: any() 694 | def head(queue) do 695 | :queue.head(queue) 696 | end 697 | 698 | @doc """ 699 | Please see `head/1`. 700 | """ 701 | @spec first(queue_type()) :: any() 702 | def first(queue) do 703 | :queue.head(queue) 704 | end 705 | 706 | @doc """ 707 | Returns a queue `q2` that is the result of removing the tail item from `q1`. 708 | 709 | ### Example: 710 | ```elixir 711 | init(from_list([1, 2, 3])) 712 | # {[2],[1]} 713 | ``` 714 | 715 | > **Fails with reason empty if `queue` is empty.** You must check if it is empty before initing it. 716 | 717 | ### Error output: 718 | 719 | ``` 720 | ** (ErlangError) Erlang error: :empty 721 | (stdlib 5.2.1) queue.erl:270: :queue.drop_r({[], []}) 722 | iex:67: (file) 723 | ``` 724 | """ 725 | @spec init(queue_type()) :: queue_type() 726 | def init(queue) do 727 | :queue.init(queue) 728 | end 729 | 730 | @doc """ 731 | Returns the tail item of a queue. 732 | 733 | ### Example: 734 | ```elixir 735 | last(from_list([1,2 , 3])) 736 | # 3 737 | ``` 738 | 739 | > **Fails with reason empty if `queue` is empty.** You must check if it is empty before getting it. 740 | 741 | ### Error output: 742 | 743 | ``` 744 | ** (ErlangError) Erlang error: :empty 745 | (stdlib 5.2.1) queue.erl:207: :queue.get_r({[], []}) 746 | iex:67: (file) 747 | ``` 748 | """ 749 | @spec last(queue_type()) :: any() 750 | def last(queue) do 751 | :queue.last(queue) 752 | end 753 | 754 | @spec liat(queue_type()) :: queue_type() 755 | @doc """ 756 | It is like `init/1`. 757 | """ 758 | @spec liat(queue_type()) :: queue_type() 759 | def liat(queue) do 760 | :queue.liat(queue) 761 | end 762 | 763 | @doc """ 764 | Inserts Item as the tail item of a queue `q1`. Returns a new queue like `q2`. 765 | 766 | ### Example: 767 | ```elixir 768 | queue = snoc(from_list([1, 2, 3]), 4) 769 | # {[4, 3, 2], [1]} 770 | 771 | to_list(queue) 772 | # [1,2,3,4] 773 | ``` 774 | """ 775 | @spec snoc(queue_type(), any()) :: queue_type() 776 | def snoc(queue, item) do 777 | :queue.snoc(queue, item) 778 | end 779 | 780 | @doc """ 781 | Returns a queue `q2` that is the result of removing the head item from `q1`. 782 | 783 | > **Fails with reason empty if `queue` is empty.** You must check if it is empty before tailing it. 784 | 785 | ### Error output: 786 | 787 | ``` 788 | ** (ErlangError) Erlang error: :empty 789 | (stdlib 5.2.1) queue.erl:252: :queue.drop({[], []}) 790 | iex:67: (file) 791 | ``` 792 | """ 793 | @spec tail(queue_type()) :: queue_type() 794 | def tail(queue) do 795 | :queue.tail(queue) 796 | end 797 | end 798 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule MishkaDeveloperTools.MixProject do 2 | use Mix.Project 3 | @version "0.1.8" 4 | @source_url "https://github.com/mishka-group/mishka_developer_tools" 5 | 6 | def project do 7 | [ 8 | app: :mishka_developer_tools, 9 | version: @version, 10 | elixir: "~> 1.15", 11 | name: "Mishka developer tools", 12 | elixirc_paths: elixirc_paths(Mix.env()), 13 | start_permanent: Mix.env() == :prod, 14 | compilers: [:yecc, :leex] ++ Mix.compilers(), 15 | deps: deps(), 16 | description: description(), 17 | package: package(), 18 | homepage_url: "https://github.com/mishka-group", 19 | source_url: @source_url, 20 | test_elixirc_options: [debug_info: Mix.env() == :test], 21 | docs: [ 22 | main: "MishkaDeveloperTools", 23 | source_ref: "master", 24 | extras: ["README.md"], 25 | source_url: @source_url 26 | ] 27 | ] 28 | end 29 | 30 | # Run "mix help compile.app" to learn about applications. 31 | def application do 32 | [ 33 | extra_applications: [:logger, :mnesia], 34 | mod: {MishkaDeveloperTools.Application, []} 35 | ] 36 | end 37 | 38 | # Run "mix help deps" to learn about dependencies. 39 | defp deps do 40 | [ 41 | {:nimble_totp, "~> 1.0", optional: true}, 42 | {:joken, "~> 2.6", optional: true}, 43 | {:jason, "~> 1.4", optional: true}, 44 | {:plug, "~> 1.17", optional: true}, 45 | # Make sure you have a C compiler installed. See the Comeonin wiki for details. 46 | # Wiki link: https://github.com/riverrun/comeonin/wiki/Requirements 47 | {:bcrypt_elixir, "~> 3.2", optional: true}, 48 | {:pbkdf2_elixir, "~> 2.3", optional: true}, 49 | {:argon2_elixir, "~> 4.1", optional: true}, 50 | # Dev dependencies 51 | {:ex_doc, "~> 0.37", only: :dev, runtime: false} 52 | ] 53 | end 54 | 55 | defp elixirc_paths(:test), do: ["lib", "test/support"] 56 | defp elixirc_paths(_), do: ["lib"] 57 | 58 | defp description() do 59 | "Mishka developer tools provides some macros and modules to make creating your elixir application as easy as possible" 60 | end 61 | 62 | defp package() do 63 | [ 64 | files: ~w(lib .formatter.exs mix.exs LICENSE README*), 65 | licenses: ["Apache-2.0"], 66 | maintainers: ["Shahryar Tavakkoli"], 67 | links: %{ 68 | "GitHub" => @source_url, 69 | "Changelog" => "#{@source_url}/blob/master/CHANGELOG.md" 70 | } 71 | ] 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "argon2_elixir": {:hex, :argon2_elixir, "4.1.2", "1160a3ccd59b951175525882240651f5ed3303b75c616204713f8b31c76b37bd", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "9222341e1b0d9aa5ca7e26a1c77bd1bd92d2314c92b57ca3e2c7ed847223b51d"}, 3 | "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.2.1", "e361261a0401d82dadc1ab7b969f91d250bf7577283e933fe8c5b72f8f5b3c46", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "81170177d5c2e280d12141a0b9d9e299bf731535e2d959982bdcd4cfe3c82865"}, 4 | "comeonin": {:hex, :comeonin, "5.5.1", "5113e5f3800799787de08a6e0db307133850e635d34e9fab23c70b6501669510", [:mix], [], "hexpm", "65aac8f19938145377cee73973f192c5645873dcf550a8a6b18187d17c13ccdb"}, 5 | "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, 6 | "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, 7 | "email_checker": {:hex, :email_checker, "0.2.4", "2bf246646678c8a366f2f6d2394845facb87c025ceddbd699019d387726548aa", [:mix], [{:socket, "~> 0.3.1", [hex: :socket, repo: "hexpm", optional: true]}], "hexpm", "e4ac0e5eb035dce9c8df08ebffdb525a5d82e61dde37390ac2469222f723e50a"}, 8 | "ex_doc": {:hex, :ex_doc, "0.37.3", "f7816881a443cd77872b7d6118e8a55f547f49903aef8747dbcb345a75b462f9", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "e6aebca7156e7c29b5da4daa17f6361205b2ae5f26e5c7d8ca0d3f7e18972233"}, 9 | "ex_phone_number": {:hex, :ex_phone_number, "0.4.4", "8e994abe583496a3308cf56af013dca9b47a0424b0a9940af41cb0d66b848dd3", [:mix], [{:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: false]}], "hexpm", "a59875692ec57b3392959a7740f3e9a5cb08da88bcaee4efd480c770f5bb0f2c"}, 10 | "ex_url": {:hex, :ex_url, "2.0.0", "c940f034895103fc493948ef0036c9473c396988dad3835531fb0c1476ba63c4", [:mix], [{:ex_cldr, "~> 2.18", [hex: :ex_cldr, repo: "hexpm", optional: true]}, {:ex_phone_number, "~> 0.1", [hex: :ex_phone_number, repo: "hexpm", optional: true]}, {:gettext, "~> 0.13", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5bba73e551462f891a3cc47d7c1a42acff8dc0cb4dce02f03c937947db4f0d31"}, 11 | "html_sanitize_ex": {:hex, :html_sanitize_ex, "1.4.3", "67b3d9fa8691b727317e0cc96b9b3093be00ee45419ffb221cdeee88e75d1360", [:mix], [{:mochiweb, "~> 2.15 or ~> 3.1", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm", "87748d3c4afe949c7c6eb7150c958c2bcba43fc5b2a02686af30e636b74bccb7"}, 12 | "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, 13 | "joken": {:hex, :joken, "2.6.2", "5daaf82259ca603af4f0b065475099ada1b2b849ff140ccd37f4b6828ca6892a", [:mix], [{:jose, "~> 1.11.10", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5134b5b0a6e37494e46dbf9e4dad53808e5e787904b7c73972651b51cce3d72b"}, 14 | "jose": {:hex, :jose, "1.11.10", "a903f5227417bd2a08c8a00a0cbcc458118be84480955e8d251297a425723f83", [:mix, :rebar3], [], "hexpm", "0d6cd36ff8ba174db29148fc112b5842186b68a90ce9fc2b3ec3afe76593e614"}, 15 | "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, 16 | "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, 17 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, 18 | "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, 19 | "mochiweb": {:hex, :mochiweb, "3.2.2", "bb435384b3b9fd1f92f2f3fe652ea644432877a3e8a81ed6459ce951e0482ad3", [:rebar3], [], "hexpm", "4114e51f1b44c270b3242d91294fe174ce1ed989100e8b65a1fab58e0cba41d5"}, 20 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, 21 | "nimble_totp": {:hex, :nimble_totp, "1.0.0", "79753bae6ce59fd7cacdb21501a1dbac249e53a51c4cd22b34fa8438ee067283", [:mix], [], "hexpm", "6ce5e4c068feecdb782e85b18237f86f66541523e6bad123e02ee1adbe48eda9"}, 22 | "pbkdf2_elixir": {:hex, :pbkdf2_elixir, "2.3.1", "073866b593887365d0ff50bb806d860a50f454bcda49b5b6f4658c9173c53889", [:mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}], "hexpm", "ab4da7db8aeb2db20e02a1d416cbb46d0690658aafb4396878acef8748c9c319"}, 23 | "plug": {:hex, :plug, "1.17.0", "a0832e7af4ae0f4819e0c08dd2e7482364937aea6a8a997a679f2cbb7e026b2e", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f6692046652a69a00a5a21d0b7e11fcf401064839d59d6b8787f23af55b1e6bc"}, 24 | "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, 25 | "sweet_xml": {:hex, :sweet_xml, "0.7.4", "a8b7e1ce7ecd775c7e8a65d501bc2cd933bff3a9c41ab763f5105688ef485d08", [:mix], [], "hexpm", "e7c4b0bdbf460c928234951def54fe87edf1a170f6896675443279e2dbeba167"}, 26 | "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, 27 | } 28 | -------------------------------------------------------------------------------- /test/mishka_developer_tools_test.exs: -------------------------------------------------------------------------------- 1 | defmodule MishkaDeveloperToolsTest do 2 | use ExUnit.Case 3 | doctest MishkaDeveloperTools 4 | end 5 | -------------------------------------------------------------------------------- /test/permission_access_test.exs: -------------------------------------------------------------------------------- 1 | defmodule MishkaDeveloperToolsTest.PermissionAccessTest do 2 | use ExUnit.Case, async: true 3 | 4 | @admin_router %{ 5 | # admin router 6 | "AdminDashboardLive" => "admin:*", 7 | "AdminBlogPostsLive" => "admin:edit", 8 | "AdminBlogPostLive" => "admin:edit", 9 | "AdminBlogCategoriesLive" => "admin:edit", 10 | "AdminBlogCategoryLive" => "admin:edit", 11 | "AdminBookmarksLive" => "*", 12 | "AdminSubscriptionsLive" => "*", 13 | "AdminSubscriptionLive" => "*", 14 | "AdminCommentsLive" => "admin:edit", 15 | "AdminCommentLive" => "admin:edit", 16 | "AdminUsersLive" => "*", 17 | "AdminUserLive" => "*", 18 | "AdminLogsLive" => "*", 19 | "AdminSeoLive" => "*", 20 | "AdminBlogPostAuthorsLive" => "admin:edit", 21 | "AdminBlogNotifLive" => "*", 22 | "AdminBlogNotifsLive" => "*" 23 | } 24 | 25 | test "User has PermissionAccess?" do 26 | result = 27 | Enum.map(Map.keys(@admin_router), fn item -> 28 | user_action = [%{value: "*"}] 29 | PermissionAccess.permittes?(user_action, @admin_router[item]) 30 | end) 31 | |> Enum.all?() 32 | 33 | assert result 34 | 35 | result1 = 36 | Enum.map(Map.keys(@admin_router), fn item -> 37 | user_action = [%{value: "admin:edit"}] 38 | PermissionAccess.permittes?(user_action, @admin_router[item]) 39 | end) 40 | |> Enum.all?() 41 | 42 | assert !result1 43 | 44 | assert !PermissionAccess.permittes?( 45 | [%{value: "admin:blog"}], 46 | @admin_router["AdminCommentsLive"] 47 | ) 48 | 49 | assert PermissionAccess.permittes?( 50 | [%{value: "*:edit"}], 51 | @admin_router["AdminBlogCategoryLive"] 52 | ) 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | 3 | defmodule User do 4 | defstruct name: "Shahryar" 5 | end 6 | 7 | defmodule ConditionalFieldValidatorTestValidators do 8 | def is_string_data(field, value) do 9 | if is_binary(value), do: {:ok, field, value}, else: {:error, field, "It is not string"} 10 | end 11 | 12 | def is_map_data(field, value) do 13 | if is_map(value), do: {:ok, field, value}, else: {:error, field, "It is not map"} 14 | end 15 | 16 | def is_list_data(field, value) do 17 | if is_list(value), do: {:ok, field, value}, else: {:error, field, "It is not list"} 18 | end 19 | 20 | def is_flat_list_data(field, value) do 21 | if is_list(value), 22 | do: {:ok, field, List.flatten(value)}, 23 | else: {:error, field, "It is not list"} 24 | end 25 | 26 | def is_int_data(field, value) do 27 | if is_integer(value), do: {:ok, field, value}, else: {:error, field, "It is not integer"} 28 | end 29 | end 30 | --------------------------------------------------------------------------------