aven 0.0.3
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 +19 -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/channels/aven/chat/thread_channel.rb +39 -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/articles/edit/component.html.erb +14 -0
- data/app/components/aven/views/articles/edit/component.rb +14 -0
- data/app/components/aven/views/articles/form/component.html.erb +45 -0
- data/app/components/aven/views/articles/form/component.rb +27 -0
- data/app/components/aven/views/articles/index/component.html.erb +93 -0
- data/app/components/aven/views/articles/index/component.rb +29 -0
- data/app/components/aven/views/articles/new/component.html.erb +13 -0
- data/app/components/aven/views/articles/new/component.rb +14 -0
- data/app/components/aven/views/articles/show/component.html.erb +110 -0
- data/app/components/aven/views/articles/show/component.rb +34 -0
- data/app/components/aven/views/oauth/error/component.html.erb +44 -0
- data/app/components/aven/views/oauth/error/component.rb +30 -0
- data/app/components/aven/views/static/index/component.html.erb +17 -0
- data/app/components/aven/views/static/index/component.rb +16 -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/agentic/agents_controller.rb +56 -0
- data/app/controllers/aven/agentic/documents_controller.rb +51 -0
- data/app/controllers/aven/agentic/mcp_controller.rb +124 -0
- data/app/controllers/aven/agentic/tools_controller.rb +37 -0
- data/app/controllers/aven/ai/text_controller.rb +41 -0
- data/app/controllers/aven/application_controller.rb +27 -0
- data/app/controllers/aven/articles_controller.rb +114 -0
- data/app/controllers/aven/auth_controller.rb +12 -0
- data/app/controllers/aven/chat/threads_controller.rb +67 -0
- data/app/controllers/aven/oauth/auth0_controller.rb +84 -0
- data/app/controllers/aven/oauth/base_controller.rb +183 -0
- data/app/controllers/aven/oauth/documentation/auth0.md +387 -0
- data/app/controllers/aven/oauth/documentation/entra_id.md +608 -0
- data/app/controllers/aven/oauth/documentation/github.md +329 -0
- data/app/controllers/aven/oauth/documentation/google.md +253 -0
- data/app/controllers/aven/oauth/entra_id_controller.rb +92 -0
- data/app/controllers/aven/oauth/github_controller.rb +91 -0
- data/app/controllers/aven/oauth/google_controller.rb +64 -0
- data/app/controllers/aven/static_controller.rb +7 -0
- data/app/controllers/aven/tags_controller.rb +44 -0
- data/app/controllers/aven/workspaces_controller.rb +20 -0
- data/app/controllers/concerns/aven/authentication.rb +49 -0
- data/app/controllers/concerns/aven/controller_helpers.rb +38 -0
- data/app/helpers/aven/application_helper.rb +16 -0
- data/app/javascript/aven/application.js +3 -0
- data/app/javascript/aven/controllers/application.js +5 -0
- data/app/javascript/aven/controllers/index.js +11 -0
- data/app/jobs/aven/agentic/document_embedding_job.rb +28 -0
- data/app/jobs/aven/agentic/document_ocr_job.rb +28 -0
- data/app/jobs/aven/application_job.rb +4 -0
- data/app/jobs/aven/chat/calculate_cost_job.rb +26 -0
- data/app/jobs/aven/chat/run_job.rb +27 -0
- data/app/mailers/aven/application_mailer.rb +6 -0
- data/app/models/aven/agentic/agent.rb +76 -0
- data/app/models/aven/agentic/agent_document.rb +37 -0
- data/app/models/aven/agentic/agent_tool.rb +37 -0
- data/app/models/aven/agentic/document.rb +162 -0
- data/app/models/aven/agentic/document_embedding.rb +39 -0
- data/app/models/aven/agentic/tool.rb +106 -0
- data/app/models/aven/agentic/tool_parameter.rb +56 -0
- data/app/models/aven/application_record.rb +5 -0
- data/app/models/aven/article.rb +86 -0
- data/app/models/aven/article_attachment.rb +18 -0
- data/app/models/aven/article_relationship.rb +26 -0
- data/app/models/aven/chat/message.rb +135 -0
- data/app/models/aven/chat/thread.rb +159 -0
- data/app/models/aven/import/entry.rb +45 -0
- data/app/models/aven/import/item_link.rb +36 -0
- data/app/models/aven/import/processor.rb +123 -0
- data/app/models/aven/import.rb +102 -0
- data/app/models/aven/item/embed.rb +54 -0
- data/app/models/aven/item/embeddable.rb +141 -0
- data/app/models/aven/item/linkable.rb +212 -0
- data/app/models/aven/item/schema/builder.rb +139 -0
- data/app/models/aven/item/schemaed.rb +252 -0
- data/app/models/aven/item/schemas/base.rb +108 -0
- data/app/models/aven/item.rb +128 -0
- data/app/models/aven/item_link.rb +43 -0
- data/app/models/aven/item_schema.rb +87 -0
- data/app/models/aven/log.rb +66 -0
- data/app/models/aven/loggable.rb +20 -0
- data/app/models/aven/user.rb +40 -0
- data/app/models/aven/workspace.rb +93 -0
- data/app/models/aven/workspace_role.rb +46 -0
- data/app/models/aven/workspace_user.rb +54 -0
- data/app/models/aven/workspace_user_role.rb +38 -0
- data/app/models/concerns/aven/agentic/document_embeddable.rb +58 -0
- data/app/models/concerns/aven/searchable.rb +61 -0
- data/app/services/aven/agentic/dynamic_tool_builder.rb +81 -0
- data/app/services/aven/agentic/mcp/adapter.rb +77 -0
- data/app/services/aven/agentic/mcp/result_formatter.rb +57 -0
- data/app/services/aven/agentic/mcp/server_factory.rb +43 -0
- data/app/services/aven/agentic/ocr/base_extractor.rb +39 -0
- data/app/services/aven/agentic/ocr/excel_extractor.rb +43 -0
- data/app/services/aven/agentic/ocr/image_extractor.rb +22 -0
- data/app/services/aven/agentic/ocr/pdf_extractor.rb +48 -0
- data/app/services/aven/agentic/ocr/processor.rb +36 -0
- data/app/services/aven/agentic/ocr/textract_client.rb +131 -0
- data/app/services/aven/agentic/ocr/word_extractor.rb +34 -0
- data/app/services/aven/agentic/tool_result_formatter.rb +76 -0
- data/app/services/aven/agentic/tools/base.rb +55 -0
- data/app/services/aven/agentic/tools/concerns/boolean_filtering.rb +40 -0
- data/app/services/aven/agentic/tools/concerns/enum_filtering.rb +47 -0
- data/app/services/aven/agentic/tools/concerns/geo_filtering.rb +56 -0
- data/app/services/aven/agentic/tools/concerns/range_filtering.rb +51 -0
- data/app/services/aven/chat/broadcaster.rb +59 -0
- data/app/services/aven/chat/config.rb +93 -0
- data/app/services/aven/chat/message_builder.rb +42 -0
- data/app/services/aven/chat/orchestrator.rb +69 -0
- data/app/services/aven/chat/runner.rb +105 -0
- data/app/services/aven/chat/title_generator.rb +61 -0
- data/app/services/aven/external/gmail_client.rb +173 -0
- data/app/services/aven/external/google_contacts_client.rb +95 -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/routes.rb +63 -0
- data/db/migrate/20200101000001_create_aven_users.rb +19 -0
- data/db/migrate/20200101000002_create_aven_workspaces.rb +14 -0
- data/db/migrate/20200101000003_create_aven_workspace_users.rb +12 -0
- data/db/migrate/20200101000004_create_aven_workspace_roles.rb +13 -0
- data/db/migrate/20200101000005_create_aven_workspace_user_roles.rb +12 -0
- data/db/migrate/20200101000006_create_aven_logs.rb +21 -0
- data/db/migrate/20200101000009_create_aven_items.rb +17 -0
- data/db/migrate/20200101000010_create_aven_item_links.rb +17 -0
- data/db/migrate/20200101000011_create_aven_agentic_tools.rb +19 -0
- data/db/migrate/20200101000012_create_aven_agentic_tool_parameters.rb +20 -0
- data/db/migrate/20200101000013_create_aven_agentic_documents.rb +22 -0
- data/db/migrate/20200101000014_create_aven_agentic_document_embeddings.rb +18 -0
- data/db/migrate/20200101000015_create_aven_agentic_agents.rb +18 -0
- data/db/migrate/20200101000016_create_aven_agentic_agent_tools.rb +13 -0
- data/db/migrate/20200101000017_create_aven_agentic_agent_documents.rb +13 -0
- data/db/migrate/20200101000018_create_aven_chat_threads.rb +19 -0
- data/db/migrate/20200101000019_create_aven_chat_messages.rb +26 -0
- data/db/migrate/20200101000020_add_pg_search_support.rb +21 -0
- data/db/migrate/20200101000021_create_aven_item_schemas.rb +18 -0
- data/db/migrate/20200101000022_create_aven_imports.rb +23 -0
- data/db/migrate/20200101000023_create_aven_import_entries.rb +13 -0
- data/db/migrate/20200101000024_create_aven_import_item_links.rb +13 -0
- data/db/migrate/20200101000025_create_aven_articles.rb +19 -0
- data/db/migrate/20200101000026_create_aven_article_attachments.rb +13 -0
- data/db/migrate/20200101000027_create_aven_article_relationships.rb +15 -0
- data/lib/aven/configuration.rb +87 -0
- data/lib/aven/engine.rb +43 -0
- data/lib/aven/model/tenant_model.rb +91 -0
- data/lib/aven/model.rb +6 -0
- data/lib/aven/version.rb +3 -0
- data/lib/aven.rb +8 -0
- data/lib/tasks/annotate_rb.rake +10 -0
- data/lib/tasks/aven_tasks.rake +21 -0
- metadata +426 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aven
|
|
4
|
+
module Item::Linkable
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
has_many :outgoing_links, class_name: "Aven::ItemLink", foreign_key: :source_id, dependent: :destroy
|
|
9
|
+
has_many :incoming_links, class_name: "Aven::ItemLink", foreign_key: :target_id, dependent: :destroy
|
|
10
|
+
|
|
11
|
+
after_save :persist_pending_links
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def persist_pending_links
|
|
17
|
+
persist_pending_link_ids if @_pending_links.present?
|
|
18
|
+
persist_pending_link_attrs if @_pending_link_attrs.present?
|
|
19
|
+
|
|
20
|
+
@_pending_links = nil
|
|
21
|
+
@_pending_link_attrs = nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def persist_pending_link_ids
|
|
25
|
+
@_pending_links.each do |relation_name, ids|
|
|
26
|
+
config = schema_links[relation_name]
|
|
27
|
+
next unless config
|
|
28
|
+
|
|
29
|
+
# Delete existing links for this relation
|
|
30
|
+
Aven::ItemLink.where(source_id: id, relation: relation_name.to_s).delete_all
|
|
31
|
+
|
|
32
|
+
if config[:cardinality] == :many
|
|
33
|
+
Array(ids).each_with_index do |target_id, position|
|
|
34
|
+
next unless Aven::Item.exists?(target_id)
|
|
35
|
+
Aven::ItemLink.create!(source_id: id, target_id:, relation: relation_name.to_s, position:)
|
|
36
|
+
end
|
|
37
|
+
else
|
|
38
|
+
if ids.present? && Aven::Item.exists?(ids)
|
|
39
|
+
Aven::ItemLink.create!(source_id: id, target_id: ids, relation: relation_name.to_s)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def persist_pending_link_attrs
|
|
46
|
+
@_pending_link_attrs.each do |relation_name, pending_data|
|
|
47
|
+
config = pending_data[:config]
|
|
48
|
+
attrs_list = pending_data[:attrs]
|
|
49
|
+
target_schema_slug = pending_data[:target_schema_slug]
|
|
50
|
+
|
|
51
|
+
if config[:cardinality] == :many
|
|
52
|
+
persist_many_link_attrs(relation_name, attrs_list, target_schema_slug)
|
|
53
|
+
else
|
|
54
|
+
persist_one_link_attrs(relation_name, attrs_list.first, target_schema_slug)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def persist_many_link_attrs(relation_name, attrs_list, target_schema_slug)
|
|
60
|
+
attrs_list.each_with_index do |attrs, position|
|
|
61
|
+
attr_hash = attrs.with_indifferent_access
|
|
62
|
+
|
|
63
|
+
if destroy_link_flag?(attr_hash)
|
|
64
|
+
# Delete the link (not the target item)
|
|
65
|
+
if attr_hash[:id].present?
|
|
66
|
+
Aven::ItemLink.where(source_id: id, target_id: attr_hash[:id], relation: relation_name.to_s).delete_all
|
|
67
|
+
end
|
|
68
|
+
elsif attr_hash[:id].present?
|
|
69
|
+
# Update existing
|
|
70
|
+
target = Aven::Item.find_by(id: attr_hash[:id])
|
|
71
|
+
if target
|
|
72
|
+
update_item_data(target, attr_hash.except(:id, :_destroy))
|
|
73
|
+
# Update position if link exists
|
|
74
|
+
link = Aven::ItemLink.find_by(source_id: id, target_id: target.id, relation: relation_name.to_s)
|
|
75
|
+
link&.update!(position:)
|
|
76
|
+
end
|
|
77
|
+
else
|
|
78
|
+
# Create new
|
|
79
|
+
next if attr_hash.except(:_destroy).blank?
|
|
80
|
+
|
|
81
|
+
data_attrs, nested_attrs = split_attrs(attr_hash)
|
|
82
|
+
target = Aven::Item.create!(
|
|
83
|
+
workspace:,
|
|
84
|
+
schema_slug: target_schema_slug,
|
|
85
|
+
data: data_attrs
|
|
86
|
+
)
|
|
87
|
+
# Apply nested attributes after creation
|
|
88
|
+
apply_nested_attrs(target, nested_attrs)
|
|
89
|
+
Aven::ItemLink.create!(source_id: id, target_id: target.id, relation: relation_name.to_s, position:)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def persist_one_link_attrs(relation_name, attrs, target_schema_slug)
|
|
95
|
+
return unless attrs
|
|
96
|
+
|
|
97
|
+
attr_hash = attrs.with_indifferent_access
|
|
98
|
+
|
|
99
|
+
if destroy_link_flag?(attr_hash)
|
|
100
|
+
# Delete the link
|
|
101
|
+
Aven::ItemLink.where(source_id: id, relation: relation_name.to_s).delete_all
|
|
102
|
+
elsif attr_hash[:id].present?
|
|
103
|
+
# Update existing
|
|
104
|
+
target = Aven::Item.find_by(id: attr_hash[:id])
|
|
105
|
+
update_item_data(target, attr_hash.except(:id, :_destroy)) if target
|
|
106
|
+
else
|
|
107
|
+
# Create new - first remove existing link
|
|
108
|
+
Aven::ItemLink.where(source_id: id, relation: relation_name.to_s).delete_all
|
|
109
|
+
return if attr_hash.except(:_destroy).blank?
|
|
110
|
+
|
|
111
|
+
data_attrs, nested_attrs = split_attrs(attr_hash)
|
|
112
|
+
target = Aven::Item.create!(
|
|
113
|
+
workspace:,
|
|
114
|
+
schema_slug: target_schema_slug,
|
|
115
|
+
data: data_attrs
|
|
116
|
+
)
|
|
117
|
+
# Apply nested attributes after creation
|
|
118
|
+
apply_nested_attrs(target, nested_attrs)
|
|
119
|
+
Aven::ItemLink.create!(source_id: id, target_id: target.id, relation: relation_name.to_s)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def update_item_data(item, attrs)
|
|
124
|
+
attrs.each do |key, value|
|
|
125
|
+
key_str = key.to_s
|
|
126
|
+
# Handle nested attributes (e.g., notes_attributes)
|
|
127
|
+
if key_str.end_with?("_attributes")
|
|
128
|
+
item.send("#{key_str}=", value)
|
|
129
|
+
else
|
|
130
|
+
item.data[key_str] = value
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
item.save!
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def split_attrs(attrs)
|
|
137
|
+
# Split attrs into data fields and nested *_attributes
|
|
138
|
+
data_attrs = {}
|
|
139
|
+
nested_attrs = {}
|
|
140
|
+
|
|
141
|
+
attrs.each do |key, value|
|
|
142
|
+
key_str = key.to_s
|
|
143
|
+
next if key_str == "id" || key_str == "_destroy"
|
|
144
|
+
|
|
145
|
+
if key_str.end_with?("_attributes")
|
|
146
|
+
nested_attrs[key_str] = value
|
|
147
|
+
else
|
|
148
|
+
data_attrs[key_str] = value
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
[data_attrs, nested_attrs]
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def apply_nested_attrs(item, nested_attrs)
|
|
156
|
+
return if nested_attrs.blank?
|
|
157
|
+
|
|
158
|
+
nested_attrs.each do |key, value|
|
|
159
|
+
item.send("#{key}=", value)
|
|
160
|
+
end
|
|
161
|
+
item.save!
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def clean_link_attrs(attrs)
|
|
165
|
+
# Only return data fields (exclude id, _destroy, and *_attributes)
|
|
166
|
+
result = {}
|
|
167
|
+
attrs.each do |key, value|
|
|
168
|
+
key_str = key.to_s
|
|
169
|
+
next if key_str == "id" || key_str == "_destroy" || key_str.end_with?("_attributes")
|
|
170
|
+
result[key_str] = value
|
|
171
|
+
end
|
|
172
|
+
result
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def destroy_link_flag?(attrs)
|
|
176
|
+
val = attrs[:_destroy] || attrs["_destroy"]
|
|
177
|
+
val == "1" || val == true
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def process_link_attributes(name, attrs)
|
|
181
|
+
config = schema_links[name]
|
|
182
|
+
return unless config
|
|
183
|
+
|
|
184
|
+
# Derive target schema_slug from link name (e.g., :notes => "note", :company => "company")
|
|
185
|
+
target_schema_slug = name.to_s.singularize
|
|
186
|
+
|
|
187
|
+
@_pending_link_attrs ||= {}
|
|
188
|
+
@_pending_link_attrs[name] = {
|
|
189
|
+
config:,
|
|
190
|
+
attrs: normalize_link_attrs(attrs, config[:cardinality]),
|
|
191
|
+
target_schema_slug:
|
|
192
|
+
}
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def normalize_link_attrs(attrs, cardinality)
|
|
196
|
+
case attrs
|
|
197
|
+
when Array
|
|
198
|
+
attrs
|
|
199
|
+
when Hash
|
|
200
|
+
if attrs.keys.all? { |k| k.to_s =~ /\A\d+\z/ }
|
|
201
|
+
attrs.values
|
|
202
|
+
elsif cardinality == :many
|
|
203
|
+
[attrs]
|
|
204
|
+
else
|
|
205
|
+
[attrs]
|
|
206
|
+
end
|
|
207
|
+
else
|
|
208
|
+
[]
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aven
|
|
4
|
+
class Item::Schema::Builder
|
|
5
|
+
attr_reader :fields, :embeds, :links
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@fields = {}
|
|
9
|
+
@embeds = {}
|
|
10
|
+
@links = {}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Scalar fields
|
|
14
|
+
def string(name, **opts)
|
|
15
|
+
@fields[name] = { type: :string, **opts }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def integer(name, **opts)
|
|
19
|
+
@fields[name] = { type: :integer, **opts }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def boolean(name, **opts)
|
|
23
|
+
@fields[name] = { type: :boolean, **opts }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def date(name, **opts)
|
|
27
|
+
@fields[name] = { type: :string, format: "date", **opts }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def datetime(name, **opts)
|
|
31
|
+
@fields[name] = { type: :string, format: "date-time", **opts }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def array(name, of:, **opts)
|
|
35
|
+
@fields[name] = { type: :array, items: { type: of }, **opts }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Embeds with inline schema block
|
|
39
|
+
def embeds_many(name, &block)
|
|
40
|
+
embed_builder = EmbedBuilder.new
|
|
41
|
+
embed_builder.instance_eval(&block) if block
|
|
42
|
+
@embeds[name] = { cardinality: :many, fields: embed_builder.fields }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def embeds_one(name, &block)
|
|
46
|
+
embed_builder = EmbedBuilder.new
|
|
47
|
+
embed_builder.instance_eval(&block) if block
|
|
48
|
+
@embeds[name] = { cardinality: :one, fields: embed_builder.fields }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Links to other Items
|
|
52
|
+
def links_many(name, class_name: "Aven::Item", inverse_of: nil)
|
|
53
|
+
@links[name] = { cardinality: :many, class_name:, inverse_of: }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def links_one(name, class_name: "Aven::Item", inverse_of: nil)
|
|
57
|
+
@links[name] = { cardinality: :one, class_name:, inverse_of: }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# JSON Schema generation
|
|
61
|
+
def to_json_schema
|
|
62
|
+
props = {}
|
|
63
|
+
required = []
|
|
64
|
+
|
|
65
|
+
fields.each do |name, config|
|
|
66
|
+
props[name.to_s] = field_to_json_prop(config)
|
|
67
|
+
required << name.to_s if config[:required]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
embeds.each do |name, config|
|
|
71
|
+
embed_schema = embed_to_json_schema(config[:fields])
|
|
72
|
+
if config[:cardinality] == :many
|
|
73
|
+
props[name.to_s] = { "type" => "array", "items" => embed_schema }
|
|
74
|
+
else
|
|
75
|
+
props[name.to_s] = embed_schema
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
{ "type" => "object", "properties" => props, "required" => required }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def field_to_json_prop(config)
|
|
85
|
+
prop = { "type" => config[:type].to_s }
|
|
86
|
+
prop["format"] = config[:format] if config[:format]
|
|
87
|
+
prop["enum"] = config[:enum] if config[:enum]
|
|
88
|
+
prop["maxLength"] = config[:max_length] if config[:max_length]
|
|
89
|
+
prop["minLength"] = config[:min_length] if config[:min_length]
|
|
90
|
+
prop["items"] = { "type" => config[:items][:type].to_s } if config[:items]
|
|
91
|
+
prop
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def embed_to_json_schema(fields)
|
|
95
|
+
props = {}
|
|
96
|
+
required = []
|
|
97
|
+
|
|
98
|
+
fields.each do |name, config|
|
|
99
|
+
props[name.to_s] = field_to_json_prop(config)
|
|
100
|
+
required << name.to_s if config[:required]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
{ "type" => "object", "properties" => props, "required" => required }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Nested builder for embeds
|
|
107
|
+
class EmbedBuilder
|
|
108
|
+
attr_reader :fields
|
|
109
|
+
|
|
110
|
+
def initialize
|
|
111
|
+
@fields = {}
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def string(name, **opts)
|
|
115
|
+
@fields[name] = { type: :string, **opts }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def integer(name, **opts)
|
|
119
|
+
@fields[name] = { type: :integer, **opts }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def boolean(name, **opts)
|
|
123
|
+
@fields[name] = { type: :boolean, **opts }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def date(name, **opts)
|
|
127
|
+
@fields[name] = { type: :string, format: "date", **opts }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def datetime(name, **opts)
|
|
131
|
+
@fields[name] = { type: :string, format: "date-time", **opts }
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def array(name, of:, **opts)
|
|
135
|
+
@fields[name] = { type: :array, items: { type: of }, **opts }
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aven
|
|
4
|
+
module Item::Schemaed
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
# Runtime schema access - delegates to resolved_schema (code class or DB record)
|
|
8
|
+
def schema_fields
|
|
9
|
+
schema_source = resolved_schema
|
|
10
|
+
return {} unless schema_source
|
|
11
|
+
|
|
12
|
+
# Code class has .fields, DB record has .fields_config
|
|
13
|
+
schema_source.respond_to?(:fields) ? schema_source.fields : schema_source.fields_config
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def schema_embeds
|
|
17
|
+
schema_source = resolved_schema
|
|
18
|
+
return {} unless schema_source
|
|
19
|
+
|
|
20
|
+
schema_source.respond_to?(:embeds) ? schema_source.embeds : schema_source.embeds_config
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def schema_links
|
|
24
|
+
schema_source = resolved_schema
|
|
25
|
+
return {} unless schema_source
|
|
26
|
+
|
|
27
|
+
schema_source.respond_to?(:links) ? schema_source.links : schema_source.links_config
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def json_schema
|
|
31
|
+
resolved_schema&.to_json_schema
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Embed accessors
|
|
35
|
+
def read_embed_many(name)
|
|
36
|
+
cache_key = "@_embed_cache_#{name}"
|
|
37
|
+
return instance_variable_get(cache_key) if instance_variable_defined?(cache_key)
|
|
38
|
+
|
|
39
|
+
raw = data[name.to_s] || []
|
|
40
|
+
embeds = raw.map { |attrs| Item::Embed.new(attrs) }
|
|
41
|
+
instance_variable_set(cache_key, embeds)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def write_embed_many(name, value)
|
|
45
|
+
cache_key = "@_embed_cache_#{name}"
|
|
46
|
+
remove_instance_variable(cache_key) if instance_variable_defined?(cache_key)
|
|
47
|
+
|
|
48
|
+
data[name.to_s] = Array(value).map do |v|
|
|
49
|
+
v.is_a?(Item::Embed) ? v.to_h : v
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def read_embed_one(name)
|
|
54
|
+
cache_key = "@_embed_cache_#{name}"
|
|
55
|
+
return instance_variable_get(cache_key) if instance_variable_defined?(cache_key)
|
|
56
|
+
|
|
57
|
+
raw = data[name.to_s]
|
|
58
|
+
embed = raw ? Item::Embed.new(raw) : nil
|
|
59
|
+
instance_variable_set(cache_key, embed)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def write_embed_one(name, value)
|
|
63
|
+
cache_key = "@_embed_cache_#{name}"
|
|
64
|
+
remove_instance_variable(cache_key) if instance_variable_defined?(cache_key)
|
|
65
|
+
|
|
66
|
+
data[name.to_s] = case value
|
|
67
|
+
when Item::Embed then value.to_h
|
|
68
|
+
when Hash then value
|
|
69
|
+
when nil then nil
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def build_embed(name, attrs = {})
|
|
74
|
+
Item::Embed.new(attrs.merge(id: SecureRandom.uuid))
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Link accessors
|
|
78
|
+
def read_link_many(name)
|
|
79
|
+
return Aven::Item.none unless persisted?
|
|
80
|
+
|
|
81
|
+
Aven::Item.active
|
|
82
|
+
.joins("INNER JOIN aven_item_links ON aven_item_links.target_id = aven_items.id")
|
|
83
|
+
.where(aven_item_links: { source_id: id, relation: name.to_s })
|
|
84
|
+
.order("aven_item_links.position")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def read_link_many_ids(name)
|
|
88
|
+
return [] unless persisted?
|
|
89
|
+
|
|
90
|
+
Aven::ItemLink.where(source_id: id, relation: name.to_s)
|
|
91
|
+
.ordered
|
|
92
|
+
.pluck(:target_id)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def write_link_many_ids(name, ids)
|
|
96
|
+
@_pending_links ||= {}
|
|
97
|
+
@_pending_links[name] = Array(ids).reject(&:blank?)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def read_link_one(name)
|
|
101
|
+
return nil unless persisted?
|
|
102
|
+
|
|
103
|
+
Aven::Item.active
|
|
104
|
+
.joins("INNER JOIN aven_item_links ON aven_item_links.target_id = aven_items.id")
|
|
105
|
+
.where(aven_item_links: { source_id: id, relation: name.to_s })
|
|
106
|
+
.first
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def read_link_one_id(name)
|
|
110
|
+
return nil unless persisted?
|
|
111
|
+
|
|
112
|
+
Aven::ItemLink.find_by(source_id: id, relation: name.to_s)&.target_id
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def write_link_one_id(name, target_id)
|
|
116
|
+
@_pending_links ||= {}
|
|
117
|
+
@_pending_links[name] = target_id.presence
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Dynamic method handling based on schema_slug
|
|
121
|
+
def method_missing(method, *args, &block)
|
|
122
|
+
method_str = method.to_s
|
|
123
|
+
|
|
124
|
+
# Setter
|
|
125
|
+
if method_str.end_with?("=")
|
|
126
|
+
attr_name = method_str.chomp("=").to_sym
|
|
127
|
+
return handle_setter(attr_name, args.first) if schema_has_attribute?(attr_name)
|
|
128
|
+
# Getter
|
|
129
|
+
elsif schema_has_attribute?(method)
|
|
130
|
+
return handle_getter(method)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
super
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def respond_to_missing?(method, include_private = false)
|
|
137
|
+
method_str = method.to_s
|
|
138
|
+
attr_name = method_str.end_with?("=") ? method_str.chomp("=").to_sym : method
|
|
139
|
+
|
|
140
|
+
schema_has_attribute?(attr_name) || super
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
private
|
|
144
|
+
|
|
145
|
+
def schema_has_attribute?(name)
|
|
146
|
+
name_str = name.to_s
|
|
147
|
+
name_sym = name.to_sym
|
|
148
|
+
|
|
149
|
+
# Check for *_ids pattern for links
|
|
150
|
+
if name_str.end_with?("_ids")
|
|
151
|
+
base = name_str.chomp("_ids").pluralize.to_sym
|
|
152
|
+
return schema_links.key?(base)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Check for *_id pattern for links_one
|
|
156
|
+
if name_str.end_with?("_id")
|
|
157
|
+
base = name_str.chomp("_id").to_sym
|
|
158
|
+
return schema_links.key?(base) && schema_links[base][:cardinality] == :one
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Check for build_* pattern
|
|
162
|
+
if name_str.start_with?("build_")
|
|
163
|
+
base = name_str.delete_prefix("build_")
|
|
164
|
+
singular_base = base.to_sym
|
|
165
|
+
plural_base = base.pluralize.to_sym
|
|
166
|
+
return schema_embeds.key?(singular_base) || schema_embeds.key?(plural_base)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Check for *_attributes= pattern
|
|
170
|
+
if name_str.end_with?("_attributes")
|
|
171
|
+
base = name_str.chomp("_attributes").to_sym
|
|
172
|
+
return schema_embeds.key?(base) || schema_links.key?(base)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
schema_fields.key?(name_sym) || schema_embeds.key?(name_sym) || schema_links.key?(name_sym)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def handle_getter(name)
|
|
179
|
+
name_str = name.to_s
|
|
180
|
+
name_sym = name.to_sym
|
|
181
|
+
|
|
182
|
+
# Link IDs getter
|
|
183
|
+
if name_str.end_with?("_ids")
|
|
184
|
+
base = name_str.chomp("_ids").pluralize.to_sym
|
|
185
|
+
return read_link_many_ids(base)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Link ID getter
|
|
189
|
+
if name_str.end_with?("_id")
|
|
190
|
+
base = name_str.chomp("_id").to_sym
|
|
191
|
+
return read_link_one_id(base)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Build helper
|
|
195
|
+
if name_str.start_with?("build_")
|
|
196
|
+
return build_embed(name_str.delete_prefix("build_"))
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Embed getter
|
|
200
|
+
if schema_embeds.key?(name_sym)
|
|
201
|
+
config = schema_embeds[name_sym]
|
|
202
|
+
return config[:cardinality] == :many ? read_embed_many(name_sym) : read_embed_one(name_sym)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Link getter
|
|
206
|
+
if schema_links.key?(name_sym)
|
|
207
|
+
config = schema_links[name_sym]
|
|
208
|
+
return config[:cardinality] == :many ? read_link_many(name_sym) : read_link_one(name_sym)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Field getter
|
|
212
|
+
data[name_str]
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def handle_setter(name, value)
|
|
216
|
+
name_str = name.to_s
|
|
217
|
+
name_sym = name.to_sym
|
|
218
|
+
|
|
219
|
+
# Link IDs setter
|
|
220
|
+
if name_str.end_with?("_ids")
|
|
221
|
+
base = name_str.chomp("_ids").pluralize.to_sym
|
|
222
|
+
return write_link_many_ids(base, value)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Link ID setter
|
|
226
|
+
if name_str.end_with?("_id")
|
|
227
|
+
base = name_str.chomp("_id").to_sym
|
|
228
|
+
return write_link_one_id(base, value)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Attributes setter (nested attributes)
|
|
232
|
+
if name_str.end_with?("_attributes")
|
|
233
|
+
base = name_str.chomp("_attributes").to_sym
|
|
234
|
+
if schema_embeds.key?(base)
|
|
235
|
+
return process_embed_attributes(base, value)
|
|
236
|
+
elsif schema_links.key?(base)
|
|
237
|
+
return process_link_attributes(base, value)
|
|
238
|
+
end
|
|
239
|
+
return
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Embed setter
|
|
243
|
+
if schema_embeds.key?(name_sym)
|
|
244
|
+
config = schema_embeds[name_sym]
|
|
245
|
+
return config[:cardinality] == :many ? write_embed_many(name_sym, value) : write_embed_one(name_sym, value)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Field setter
|
|
249
|
+
data[name_str] = value
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aven
|
|
4
|
+
class Item::Schemas::Base
|
|
5
|
+
class_attribute :_builder
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
def inherited(subclass)
|
|
9
|
+
super
|
|
10
|
+
subclass._builder = Item::Schema::Builder.new
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def slug
|
|
14
|
+
name.demodulize.underscore
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def builder
|
|
18
|
+
_builder
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# DSL methods delegate to builder
|
|
22
|
+
def string(name, **opts)
|
|
23
|
+
_builder.string(name, **opts)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def integer(name, **opts)
|
|
27
|
+
_builder.integer(name, **opts)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def boolean(name, **opts)
|
|
31
|
+
_builder.boolean(name, **opts)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def date(name, **opts)
|
|
35
|
+
_builder.date(name, **opts)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def datetime(name, **opts)
|
|
39
|
+
_builder.datetime(name, **opts)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def array(name, of:, **opts)
|
|
43
|
+
_builder.array(name, of:, **opts)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def embeds_many(name, &block)
|
|
47
|
+
_builder.embeds_many(name, &block)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def embeds_one(name, &block)
|
|
51
|
+
_builder.embeds_one(name, &block)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def links_many(name, class_name: "Aven::Item", inverse_of: nil)
|
|
55
|
+
_builder.links_many(name, class_name:, inverse_of:)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def links_one(name, class_name: "Aven::Item", inverse_of: nil)
|
|
59
|
+
_builder.links_one(name, class_name:, inverse_of:)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def fields
|
|
63
|
+
_builder&.fields || {}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def embeds
|
|
67
|
+
_builder&.embeds || {}
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def links
|
|
71
|
+
_builder&.links || {}
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def to_json_schema
|
|
75
|
+
_builder&.to_json_schema
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Query helpers - delegate to Item scoped by schema_slug
|
|
79
|
+
def all
|
|
80
|
+
Aven::Item.by_schema(slug)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def where(...)
|
|
84
|
+
all.where(...)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def find(...)
|
|
88
|
+
all.find(...)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def find_by(...)
|
|
92
|
+
all.find_by(...)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def create(attrs = {})
|
|
96
|
+
Aven::Item.create(attrs.merge(schema_slug: slug))
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def create!(attrs = {})
|
|
100
|
+
Aven::Item.create!(attrs.merge(schema_slug: slug))
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def new(attrs = {})
|
|
104
|
+
Aven::Item.new(attrs.merge(schema_slug: slug))
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|