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.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +35 -0
  4. data/Rakefile +8 -0
  5. data/app/assets/stylesheets/aven/application.css +14 -0
  6. data/app/assets/stylesheets/aven/application.tailwind.css +7 -0
  7. data/app/assets/stylesheets/aven/tailwind.css +224 -0
  8. data/app/components/aven/application_view_component.rb +15 -0
  9. data/app/components/aven/views/admin/dashboard/index/component.html.erb +1 -0
  10. data/app/components/aven/views/admin/dashboard/index/component.rb +5 -0
  11. data/app/components/aven/views/static/index/component.html.erb +17 -0
  12. data/app/components/aven/views/static/index/component.rb +5 -0
  13. data/app/components/aven/views/static/index/controller.js +7 -0
  14. data/app/controllers/aven/admin/base.rb +16 -0
  15. data/app/controllers/aven/admin/dashboard_controller.rb +9 -0
  16. data/app/controllers/aven/application_controller.rb +5 -0
  17. data/app/controllers/aven/auth_controller.rb +64 -0
  18. data/app/controllers/aven/static_controller.rb +7 -0
  19. data/app/helpers/aven/application_helper.rb +20 -0
  20. data/app/javascript/sqema/application.js +3 -0
  21. data/app/javascript/sqema/controllers/application.js +5 -0
  22. data/app/javascript/sqema/controllers/index.js +11 -0
  23. data/app/jobs/aven/application_job.rb +4 -0
  24. data/app/mailers/aven/application_mailer.rb +6 -0
  25. data/app/models/aven/app_record.rb +76 -0
  26. data/app/models/aven/app_record_schema.rb +47 -0
  27. data/app/models/aven/application_record.rb +5 -0
  28. data/app/models/aven/log.rb +67 -0
  29. data/app/models/aven/loggable.rb +21 -0
  30. data/app/models/aven/user.rb +63 -0
  31. data/app/models/aven/workspace.rb +39 -0
  32. data/app/models/aven/workspace_role.rb +47 -0
  33. data/app/models/aven/workspace_user.rb +55 -0
  34. data/app/models/aven/workspace_user_role.rb +39 -0
  35. data/app/views/layouts/aven/admin.html.erb +16 -0
  36. data/app/views/layouts/aven/application.html.erb +18 -0
  37. data/config/importmap.rb +16 -0
  38. data/config/initializers/devise.rb +43 -0
  39. data/config/routes.rb +16 -0
  40. data/db/migrate/20251003090752_create_aven_users.rb +19 -0
  41. data/db/migrate/20251004182000_create_aven_workspaces.rb +14 -0
  42. data/db/migrate/20251004182010_create_aven_workspace_users.rb +12 -0
  43. data/db/migrate/20251004182020_create_aven_workspace_roles.rb +13 -0
  44. data/db/migrate/20251004182030_create_aven_workspace_user_roles.rb +12 -0
  45. data/db/migrate/20251004190000_create_aven_logs.rb +22 -0
  46. data/db/migrate/20251004190100_create_aven_app_record_schemas.rb +12 -0
  47. data/db/migrate/20251004190110_create_aven_app_records.rb +12 -0
  48. data/lib/aven/configuration.rb +35 -0
  49. data/lib/aven/engine.rb +44 -0
  50. data/lib/aven/version.rb +3 -0
  51. data/lib/aven.rb +6 -0
  52. data/lib/tasks/annotate_rb.rake +10 -0
  53. data/lib/tasks/sqema_tasks.rake +21 -0
  54. 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,8 @@
1
+ require "bundler/setup"
2
+
3
+ APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
+
6
+ load "rails/tasks/statistics.rake"
7
+
8
+ require "bundler/gem_tasks"
@@ -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,7 @@
1
+ @import "tailwindcss";
2
+
3
+ @layer base {
4
+ html {
5
+ @apply text-stone-700;
6
+ }
7
+ }
@@ -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,5 @@
1
+ module Aven::Views::Admin::Dashboard::Index
2
+ class Component < Aven::ApplicationViewComponent
3
+ option(:current_user, optional: true)
4
+ end
5
+ end
@@ -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,5 @@
1
+ module Aven::Views::Static::Index
2
+ class Component < Aven::ApplicationViewComponent
3
+ option(:current_user, optional: true)
4
+ end
5
+ end
@@ -0,0 +1,7 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class extends Controller {
4
+ connect() {
5
+ console.log("static/index connected");
6
+ }
7
+ }
@@ -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,9 @@
1
+ module Aven
2
+ module Admin
3
+ class DashboardController < Base
4
+ def index
5
+ view_component("admin/dashboard/index", current_user:)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ module Aven
2
+ class ApplicationController < ActionController::Base
3
+ include Aven::ApplicationHelper
4
+ end
5
+ 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,7 @@
1
+ module Aven
2
+ class StaticController < ApplicationController
3
+ def index
4
+ view_component("static/index", current_user:)
5
+ end
6
+ end
7
+ 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,3 @@
1
+ // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
2
+ import "@hotwired/turbo-rails";
3
+ import "aven/controllers";
@@ -0,0 +1,5 @@
1
+ import { Application } from "@hotwired/stimulus";
2
+ const application = Application.start();
3
+ application.debug = false;
4
+ window.Stimulus = application;
5
+ export { application };
@@ -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,4 @@
1
+ module Aven
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module Aven
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: "from@example.com"
4
+ layout "mailer"
5
+ end
6
+ end
@@ -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
+
@@ -0,0 +1,5 @@
1
+ module Aven
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end