├── .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 |
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 | [](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 | [](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 |
--------------------------------------------------------------------------------