aven 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +35 -0
- data/Rakefile +8 -0
- data/app/assets/stylesheets/aven/application.css +14 -0
- data/app/assets/stylesheets/aven/application.tailwind.css +7 -0
- data/app/assets/stylesheets/aven/tailwind.css +224 -0
- data/app/components/aven/application_view_component.rb +15 -0
- data/app/components/aven/views/admin/dashboard/index/component.html.erb +1 -0
- data/app/components/aven/views/admin/dashboard/index/component.rb +5 -0
- data/app/components/aven/views/static/index/component.html.erb +17 -0
- data/app/components/aven/views/static/index/component.rb +5 -0
- data/app/components/aven/views/static/index/controller.js +7 -0
- data/app/controllers/aven/admin/base.rb +16 -0
- data/app/controllers/aven/admin/dashboard_controller.rb +9 -0
- data/app/controllers/aven/application_controller.rb +5 -0
- data/app/controllers/aven/auth_controller.rb +64 -0
- data/app/controllers/aven/static_controller.rb +7 -0
- data/app/helpers/aven/application_helper.rb +20 -0
- data/app/javascript/sqema/application.js +3 -0
- data/app/javascript/sqema/controllers/application.js +5 -0
- data/app/javascript/sqema/controllers/index.js +11 -0
- data/app/jobs/aven/application_job.rb +4 -0
- data/app/mailers/aven/application_mailer.rb +6 -0
- data/app/models/aven/app_record.rb +76 -0
- data/app/models/aven/app_record_schema.rb +47 -0
- data/app/models/aven/application_record.rb +5 -0
- data/app/models/aven/log.rb +67 -0
- data/app/models/aven/loggable.rb +21 -0
- data/app/models/aven/user.rb +63 -0
- data/app/models/aven/workspace.rb +39 -0
- data/app/models/aven/workspace_role.rb +47 -0
- data/app/models/aven/workspace_user.rb +55 -0
- data/app/models/aven/workspace_user_role.rb +39 -0
- data/app/views/layouts/aven/admin.html.erb +16 -0
- data/app/views/layouts/aven/application.html.erb +18 -0
- data/config/importmap.rb +16 -0
- data/config/initializers/devise.rb +43 -0
- data/config/routes.rb +16 -0
- data/db/migrate/20251003090752_create_aven_users.rb +19 -0
- data/db/migrate/20251004182000_create_aven_workspaces.rb +14 -0
- data/db/migrate/20251004182010_create_aven_workspace_users.rb +12 -0
- data/db/migrate/20251004182020_create_aven_workspace_roles.rb +13 -0
- data/db/migrate/20251004182030_create_aven_workspace_user_roles.rb +12 -0
- data/db/migrate/20251004190000_create_aven_logs.rb +22 -0
- data/db/migrate/20251004190100_create_aven_app_record_schemas.rb +12 -0
- data/db/migrate/20251004190110_create_aven_app_records.rb +12 -0
- data/lib/aven/configuration.rb +35 -0
- data/lib/aven/engine.rb +44 -0
- data/lib/aven/version.rb +3 -0
- data/lib/aven.rb +6 -0
- data/lib/tasks/annotate_rb.rake +10 -0
- data/lib/tasks/sqema_tasks.rake +21 -0
- metadata +321 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 787e11b1022af941769fa87fa5ba94984d46cb1dad8b0920ff780ece60f2b68d
|
4
|
+
data.tar.gz: f6485456b219a405fc48d7ae645098fcda745877a7efd93a6a61522e5383b065
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a5e5a300447b6cf2768acd41bf5c16ba3f56400a8da4e3a491e8263e7127e2b1d2b3346c033281e2500671d4c9bf6e86f9286799478afa27b73ca66bee2d886e
|
7
|
+
data.tar.gz: ba97a554b0c026cb4fb3af9ae5c6b5470746f47cf3d8c74788aa2f8dcedb477b08dc9a766c8cd8c425718c0b934339f20ecee788e732f8d7b04499be5b6b50e8
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright Ben
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# Aven
|
2
|
+
|
3
|
+
Short description and motivation.
|
4
|
+
|
5
|
+
## Usage
|
6
|
+
|
7
|
+
How to use my plugin.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Add this line to your application's Gemfile:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
gem "aven"
|
15
|
+
```
|
16
|
+
|
17
|
+
And then execute:
|
18
|
+
|
19
|
+
```bash
|
20
|
+
$ bundle
|
21
|
+
```
|
22
|
+
|
23
|
+
Or install it yourself as:
|
24
|
+
|
25
|
+
```bash
|
26
|
+
$ gem install aven
|
27
|
+
```
|
28
|
+
|
29
|
+
## Contributing
|
30
|
+
|
31
|
+
Contribution directions go here.
|
32
|
+
|
33
|
+
## License
|
34
|
+
|
35
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
/*
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
3
|
+
* listed below.
|
4
|
+
*
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
6
|
+
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
7
|
+
*
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
10
|
+
* files in this directory. Styles in this file should be added after the last require_* statement.
|
11
|
+
* It is generally better to create a new file per style scope.
|
12
|
+
*
|
13
|
+
*= require_self
|
14
|
+
*/
|
@@ -0,0 +1,224 @@
|
|
1
|
+
/*! tailwindcss v4.1.13 | MIT License | https://tailwindcss.com */
|
2
|
+
@layer theme, base, components, utilities;
|
3
|
+
@layer theme {
|
4
|
+
:root, :host {
|
5
|
+
--font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
6
|
+
'Noto Color Emoji';
|
7
|
+
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
8
|
+
monospace;
|
9
|
+
--color-slate-50: oklch(98.4% 0.003 247.858);
|
10
|
+
--color-slate-100: oklch(96.8% 0.007 247.896);
|
11
|
+
--color-stone-700: oklch(37.4% 0.01 67.558);
|
12
|
+
--default-font-family: var(--font-sans);
|
13
|
+
--default-mono-font-family: var(--font-mono);
|
14
|
+
}
|
15
|
+
}
|
16
|
+
@layer base {
|
17
|
+
*, ::after, ::before, ::backdrop, ::file-selector-button {
|
18
|
+
box-sizing: border-box;
|
19
|
+
margin: 0;
|
20
|
+
padding: 0;
|
21
|
+
border: 0 solid;
|
22
|
+
}
|
23
|
+
html, :host {
|
24
|
+
line-height: 1.5;
|
25
|
+
-webkit-text-size-adjust: 100%;
|
26
|
+
tab-size: 4;
|
27
|
+
font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji');
|
28
|
+
font-feature-settings: var(--default-font-feature-settings, normal);
|
29
|
+
font-variation-settings: var(--default-font-variation-settings, normal);
|
30
|
+
-webkit-tap-highlight-color: transparent;
|
31
|
+
}
|
32
|
+
hr {
|
33
|
+
height: 0;
|
34
|
+
color: inherit;
|
35
|
+
border-top-width: 1px;
|
36
|
+
}
|
37
|
+
abbr:where([title]) {
|
38
|
+
-webkit-text-decoration: underline dotted;
|
39
|
+
text-decoration: underline dotted;
|
40
|
+
}
|
41
|
+
h1, h2, h3, h4, h5, h6 {
|
42
|
+
font-size: inherit;
|
43
|
+
font-weight: inherit;
|
44
|
+
}
|
45
|
+
a {
|
46
|
+
color: inherit;
|
47
|
+
-webkit-text-decoration: inherit;
|
48
|
+
text-decoration: inherit;
|
49
|
+
}
|
50
|
+
b, strong {
|
51
|
+
font-weight: bolder;
|
52
|
+
}
|
53
|
+
code, kbd, samp, pre {
|
54
|
+
font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace);
|
55
|
+
font-feature-settings: var(--default-mono-font-feature-settings, normal);
|
56
|
+
font-variation-settings: var(--default-mono-font-variation-settings, normal);
|
57
|
+
font-size: 1em;
|
58
|
+
}
|
59
|
+
small {
|
60
|
+
font-size: 80%;
|
61
|
+
}
|
62
|
+
sub, sup {
|
63
|
+
font-size: 75%;
|
64
|
+
line-height: 0;
|
65
|
+
position: relative;
|
66
|
+
vertical-align: baseline;
|
67
|
+
}
|
68
|
+
sub {
|
69
|
+
bottom: -0.25em;
|
70
|
+
}
|
71
|
+
sup {
|
72
|
+
top: -0.5em;
|
73
|
+
}
|
74
|
+
table {
|
75
|
+
text-indent: 0;
|
76
|
+
border-color: inherit;
|
77
|
+
border-collapse: collapse;
|
78
|
+
}
|
79
|
+
:-moz-focusring {
|
80
|
+
outline: auto;
|
81
|
+
}
|
82
|
+
progress {
|
83
|
+
vertical-align: baseline;
|
84
|
+
}
|
85
|
+
summary {
|
86
|
+
display: list-item;
|
87
|
+
}
|
88
|
+
ol, ul, menu {
|
89
|
+
list-style: none;
|
90
|
+
}
|
91
|
+
img, svg, video, canvas, audio, iframe, embed, object {
|
92
|
+
display: block;
|
93
|
+
vertical-align: middle;
|
94
|
+
}
|
95
|
+
img, video {
|
96
|
+
max-width: 100%;
|
97
|
+
height: auto;
|
98
|
+
}
|
99
|
+
button, input, select, optgroup, textarea, ::file-selector-button {
|
100
|
+
font: inherit;
|
101
|
+
font-feature-settings: inherit;
|
102
|
+
font-variation-settings: inherit;
|
103
|
+
letter-spacing: inherit;
|
104
|
+
color: inherit;
|
105
|
+
border-radius: 0;
|
106
|
+
background-color: transparent;
|
107
|
+
opacity: 1;
|
108
|
+
}
|
109
|
+
:where(select:is([multiple], [size])) optgroup {
|
110
|
+
font-weight: bolder;
|
111
|
+
}
|
112
|
+
:where(select:is([multiple], [size])) optgroup option {
|
113
|
+
padding-inline-start: 20px;
|
114
|
+
}
|
115
|
+
::file-selector-button {
|
116
|
+
margin-inline-end: 4px;
|
117
|
+
}
|
118
|
+
::placeholder {
|
119
|
+
opacity: 1;
|
120
|
+
}
|
121
|
+
@supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) {
|
122
|
+
::placeholder {
|
123
|
+
color: currentcolor;
|
124
|
+
@supports (color: color-mix(in lab, red, red)) {
|
125
|
+
color: color-mix(in oklab, currentcolor 50%, transparent);
|
126
|
+
}
|
127
|
+
}
|
128
|
+
}
|
129
|
+
textarea {
|
130
|
+
resize: vertical;
|
131
|
+
}
|
132
|
+
::-webkit-search-decoration {
|
133
|
+
-webkit-appearance: none;
|
134
|
+
}
|
135
|
+
::-webkit-date-and-time-value {
|
136
|
+
min-height: 1lh;
|
137
|
+
text-align: inherit;
|
138
|
+
}
|
139
|
+
::-webkit-datetime-edit {
|
140
|
+
display: inline-flex;
|
141
|
+
}
|
142
|
+
::-webkit-datetime-edit-fields-wrapper {
|
143
|
+
padding: 0;
|
144
|
+
}
|
145
|
+
::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {
|
146
|
+
padding-block: 0;
|
147
|
+
}
|
148
|
+
::-webkit-calendar-picker-indicator {
|
149
|
+
line-height: 1;
|
150
|
+
}
|
151
|
+
:-moz-ui-invalid {
|
152
|
+
box-shadow: none;
|
153
|
+
}
|
154
|
+
button, input:where([type='button'], [type='reset'], [type='submit']), ::file-selector-button {
|
155
|
+
appearance: button;
|
156
|
+
}
|
157
|
+
::-webkit-inner-spin-button, ::-webkit-outer-spin-button {
|
158
|
+
height: auto;
|
159
|
+
}
|
160
|
+
[hidden]:where(:not([hidden='until-found'])) {
|
161
|
+
display: none !important;
|
162
|
+
}
|
163
|
+
}
|
164
|
+
@layer utilities {
|
165
|
+
.visible {
|
166
|
+
visibility: visible;
|
167
|
+
}
|
168
|
+
.absolute {
|
169
|
+
position: absolute;
|
170
|
+
}
|
171
|
+
.fixed {
|
172
|
+
position: fixed;
|
173
|
+
}
|
174
|
+
.relative {
|
175
|
+
position: relative;
|
176
|
+
}
|
177
|
+
.isolate {
|
178
|
+
isolation: isolate;
|
179
|
+
}
|
180
|
+
.container {
|
181
|
+
width: 100%;
|
182
|
+
@media (width >= 40rem) {
|
183
|
+
max-width: 40rem;
|
184
|
+
}
|
185
|
+
@media (width >= 48rem) {
|
186
|
+
max-width: 48rem;
|
187
|
+
}
|
188
|
+
@media (width >= 64rem) {
|
189
|
+
max-width: 64rem;
|
190
|
+
}
|
191
|
+
@media (width >= 80rem) {
|
192
|
+
max-width: 80rem;
|
193
|
+
}
|
194
|
+
@media (width >= 96rem) {
|
195
|
+
max-width: 96rem;
|
196
|
+
}
|
197
|
+
}
|
198
|
+
.block {
|
199
|
+
display: block;
|
200
|
+
}
|
201
|
+
.contents {
|
202
|
+
display: contents;
|
203
|
+
}
|
204
|
+
.hidden {
|
205
|
+
display: none;
|
206
|
+
}
|
207
|
+
.inline {
|
208
|
+
display: inline;
|
209
|
+
}
|
210
|
+
.table {
|
211
|
+
display: table;
|
212
|
+
}
|
213
|
+
.bg-slate-50 {
|
214
|
+
background-color: var(--color-slate-50);
|
215
|
+
}
|
216
|
+
.bg-slate-100 {
|
217
|
+
background-color: var(--color-slate-100);
|
218
|
+
}
|
219
|
+
}
|
220
|
+
@layer base {
|
221
|
+
html {
|
222
|
+
color: var(--color-stone-700);
|
223
|
+
}
|
224
|
+
}
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Aven
|
2
|
+
class ApplicationViewComponent < Aeros::ApplicationViewComponent
|
3
|
+
def controller_name
|
4
|
+
# Match JS autoload naming for components/controllers:
|
5
|
+
# - aven/controllers/hello_controller -> aven--hello
|
6
|
+
# - aven/components/views/static/index/controller -> aven--views--static--index
|
7
|
+
name = self.class.name
|
8
|
+
.sub(/^Aven::/, "")
|
9
|
+
.sub(/::Component$/, "")
|
10
|
+
.underscore
|
11
|
+
|
12
|
+
"aven--#{name.gsub('/', '--').gsub('_', '-')}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
ok admin
|
@@ -0,0 +1,17 @@
|
|
1
|
+
<%= ui("button", label: "ok") %>
|
2
|
+
|
3
|
+
<div data-controller="<%= controller_name %>"></div>
|
4
|
+
|
5
|
+
<% if current_user %>
|
6
|
+
<div><%= current_user.id %></div>
|
7
|
+
<%= link_to("Logout", helpers.logout_path) %>
|
8
|
+
<% else %>
|
9
|
+
<% Aven.configuration.auth.providers.each do |provider_config| %>
|
10
|
+
<%=
|
11
|
+
link_to(
|
12
|
+
"Login with #{provider_config[:provider]}",
|
13
|
+
helpers.authenticate_path(provider: provider_config[:provider])
|
14
|
+
)
|
15
|
+
%>
|
16
|
+
<% end %>
|
17
|
+
<% end %>
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Aven
|
2
|
+
module Admin
|
3
|
+
class Base < ApplicationController
|
4
|
+
layout("aven/admin")
|
5
|
+
before_action(:authenticate_admin!)
|
6
|
+
|
7
|
+
private
|
8
|
+
|
9
|
+
def authenticate_admin!
|
10
|
+
unless current_user&.admin
|
11
|
+
redirect_to(root_path)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Aven
|
4
|
+
class AuthController < ApplicationController
|
5
|
+
def authenticate
|
6
|
+
provider = params[:provider].to_s
|
7
|
+
|
8
|
+
raise(StandardError, "invalid provider") unless configured_providers.include?(provider)
|
9
|
+
|
10
|
+
redirect_post(
|
11
|
+
send("user_#{provider}_omniauth_authorize_path"),
|
12
|
+
params: { authenticity_token: form_authenticity_token }
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
def action_missing(action_name)
|
17
|
+
if configured_providers.include?(action_name.to_s)
|
18
|
+
handle_omniauth(action_name.to_s)
|
19
|
+
else
|
20
|
+
raise AbstractController::ActionNotFound, "The action '#{action_name}' could not be found for #{self.class.name}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def passthru
|
25
|
+
logout if request.method == "GET"
|
26
|
+
end
|
27
|
+
|
28
|
+
def failure
|
29
|
+
logout if request.method == "GET"
|
30
|
+
end
|
31
|
+
|
32
|
+
def logout
|
33
|
+
sign_out(current_user) if current_user
|
34
|
+
reset_session
|
35
|
+
redirect_to after_sign_out_path_for(nil)
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def configured_providers
|
41
|
+
@configured_providers ||= Aven.configuration.auth.providers.map { |p| p[:provider].to_s }
|
42
|
+
end
|
43
|
+
|
44
|
+
def handle_omniauth(kind)
|
45
|
+
auth_tenant = request.host # or however you determine tenant
|
46
|
+
user = Aven::User.create_from_omniauth!(request.env, auth_tenant)
|
47
|
+
|
48
|
+
if user.persisted?
|
49
|
+
sign_in_and_redirect user, event: :authentication
|
50
|
+
else
|
51
|
+
session["devise.auth"] = request.env["omniauth.auth"].except(:extra)
|
52
|
+
redirect_to new_user_registration_url
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def after_sign_in_path_for(resource)
|
57
|
+
stored_location_for(resource) || Aven.configuration.authenticated_root_path || root_path
|
58
|
+
end
|
59
|
+
|
60
|
+
def after_sign_out_path_for(resource_or_scope)
|
61
|
+
Aven.configuration.authenticated_root_path || root_path
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Aven
|
2
|
+
module ApplicationHelper
|
3
|
+
def aven_importmap_tags(entry_point = "application", shim: true)
|
4
|
+
safe_join [
|
5
|
+
javascript_inline_importmap_tag(Aven.importmap.to_json(resolver: self)),
|
6
|
+
javascript_importmap_module_preload_tags(Aven.importmap),
|
7
|
+
javascript_import_module_tag(entry_point)
|
8
|
+
].compact, "\n"
|
9
|
+
end
|
10
|
+
|
11
|
+
def view_component(name, *args, status: nil, **kwargs, &block)
|
12
|
+
component = "Aven::Views::#{name.split("/").map(&:camelize).join("::")}::Component".constantize
|
13
|
+
if status
|
14
|
+
render(component.new(*args, **kwargs), status:, &block)
|
15
|
+
else
|
16
|
+
render(component.new(*args, **kwargs), &block)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
import { application } from "aven/controllers/application";
|
2
|
+
import { eagerLoadEngineControllersFrom } from "aeros/controllers/loader";
|
3
|
+
|
4
|
+
// Load controllers from the standard controllers directory
|
5
|
+
eagerLoadEngineControllersFrom("aven/controllers", application);
|
6
|
+
|
7
|
+
// Load component controllers
|
8
|
+
eagerLoadEngineControllersFrom("aven/components", application);
|
9
|
+
|
10
|
+
// Load UI gem component controllers
|
11
|
+
eagerLoadEngineControllersFrom("aeros/components", application);
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# == Schema Information
|
2
|
+
#
|
3
|
+
# Table name: aven_app_records
|
4
|
+
#
|
5
|
+
# id :bigint not null, primary key
|
6
|
+
# data :jsonb not null
|
7
|
+
# created_at :datetime not null
|
8
|
+
# updated_at :datetime not null
|
9
|
+
# app_record_schema_id :bigint not null
|
10
|
+
#
|
11
|
+
# Indexes
|
12
|
+
#
|
13
|
+
# index_aven_app_records_on_app_record_schema_id (app_record_schema_id)
|
14
|
+
# index_aven_app_records_on_data (data) USING gin
|
15
|
+
#
|
16
|
+
# Foreign Keys
|
17
|
+
#
|
18
|
+
# fk_rails_... (app_record_schema_id => aven_app_record_schemas.id)
|
19
|
+
#
|
20
|
+
module Aven
|
21
|
+
class AppRecord < ApplicationRecord
|
22
|
+
self.table_name = "aven_app_records"
|
23
|
+
|
24
|
+
include Aven::Loggable
|
25
|
+
|
26
|
+
belongs_to :app_record_schema, class_name: "Aven::AppRecordSchema"
|
27
|
+
has_many :logs, as: :loggable, class_name: "Aven::Log", dependent: :destroy
|
28
|
+
|
29
|
+
delegate :workspace, to: :app_record_schema
|
30
|
+
|
31
|
+
validates :data, presence: true
|
32
|
+
validate :validate_data_against_schema
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def validate_data_against_schema
|
37
|
+
if app_record_schema.blank?
|
38
|
+
errors.add(:app_record_schema, "must exist")
|
39
|
+
return
|
40
|
+
end
|
41
|
+
|
42
|
+
if app_record_schema.schema.blank?
|
43
|
+
errors.add(:app_record_schema, "schema must be present")
|
44
|
+
return
|
45
|
+
end
|
46
|
+
|
47
|
+
if data.blank?
|
48
|
+
errors.add(:data, :blank)
|
49
|
+
return
|
50
|
+
end
|
51
|
+
|
52
|
+
begin
|
53
|
+
registry = JSONSkooma.create_registry("2020-12", assert_formats: true)
|
54
|
+
schema_with_meta = app_record_schema.schema.dup
|
55
|
+
schema_with_meta["$schema"] ||= "https://json-schema.org/draft/2020-12/schema"
|
56
|
+
json_schema = JSONSkooma::JSONSchema.new(schema_with_meta, registry: registry)
|
57
|
+
result = json_schema.evaluate(data)
|
58
|
+
unless result.valid?
|
59
|
+
error_output = result.output(:basic)
|
60
|
+
if error_output["errors"]
|
61
|
+
error_messages = error_output["errors"].map do |err|
|
62
|
+
location = err["instanceLocation"] || "data"
|
63
|
+
message = err["error"] || "validation failed"
|
64
|
+
"#{location}: #{message}"
|
65
|
+
end
|
66
|
+
errors.add(:data, "schema validation failed: #{error_messages.join('; ')}")
|
67
|
+
else
|
68
|
+
errors.add(:data, "does not conform to schema")
|
69
|
+
end
|
70
|
+
end
|
71
|
+
rescue => e
|
72
|
+
errors.add(:data, "schema validation error: #{e.message}")
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# == Schema Information
|
2
|
+
#
|
3
|
+
# Table name: aven_app_record_schemas
|
4
|
+
#
|
5
|
+
# id :bigint not null, primary key
|
6
|
+
# schema :jsonb not null
|
7
|
+
# created_at :datetime not null
|
8
|
+
# updated_at :datetime not null
|
9
|
+
# workspace_id :bigint not null
|
10
|
+
#
|
11
|
+
# Indexes
|
12
|
+
#
|
13
|
+
# index_aven_app_record_schemas_on_schema (schema) USING gin
|
14
|
+
# index_aven_app_record_schemas_on_workspace_id (workspace_id)
|
15
|
+
#
|
16
|
+
# Foreign Keys
|
17
|
+
#
|
18
|
+
# fk_rails_... (workspace_id => aven_workspaces.id)
|
19
|
+
#
|
20
|
+
module Aven
|
21
|
+
class AppRecordSchema < ApplicationRecord
|
22
|
+
self.table_name = "aven_app_record_schemas"
|
23
|
+
|
24
|
+
include Aven::Loggable
|
25
|
+
|
26
|
+
belongs_to(:workspace, class_name: "Aven::Workspace")
|
27
|
+
has_many(:app_records, class_name: "Aven::AppRecord", dependent: :destroy)
|
28
|
+
has_many(:logs, as: :loggable, class_name: "Aven::Log", dependent: :destroy)
|
29
|
+
|
30
|
+
validates(:schema, presence: true)
|
31
|
+
validate(:validate_openapi_schema_format)
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def validate_openapi_schema_format
|
36
|
+
return if schema.blank?
|
37
|
+
unless schema.is_a?(Hash)
|
38
|
+
errors.add(:schema, "must be a valid JSON object")
|
39
|
+
return
|
40
|
+
end
|
41
|
+
unless schema["type"].present?
|
42
|
+
errors.add(:schema, "must include a 'type' property")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|