bridgetown_directus 0.1.3 → 0.3.0
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 +4 -4
- data/.github/workflows/ci.yml +23 -0
- data/.github/workflows/release-please.yml +20 -0
- data/.github/workflows/release.yml +28 -0
- data/.gitignore +3 -0
- data/.release-please-config.json +10 -0
- data/.release-please-manifest.json +3 -0
- data/CHANGELOG.md +17 -0
- data/README.md +75 -116
- data/Rakefile +1 -1
- data/bridgetown.automation.rb +50 -48
- data/bridgetown_directus.gemspec +2 -2
- data/example/bridgetown.config.yml +24 -0
- data/example/config/initializers.rb +34 -0
- data/lib/bridgetown_directus/builder.rb +216 -70
- data/lib/bridgetown_directus/client.rb +71 -0
- data/lib/bridgetown_directus/configuration.rb +82 -0
- data/lib/bridgetown_directus/data_mapper.rb +160 -0
- data/lib/bridgetown_directus/utils.rb +6 -1
- data/lib/bridgetown_directus/version.rb +1 -1
- data/lib/bridgetown_directus.rb +21 -19
- metadata +19 -14
- data/lib/bridgetown_directus/api_client.rb +0 -57
|
@@ -1,103 +1,249 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "yaml"
|
|
6
|
+
|
|
1
7
|
module BridgetownDirectus
|
|
2
8
|
class Builder < Bridgetown::Builder
|
|
3
9
|
def build
|
|
10
|
+
config = site.config.bridgetown_directus
|
|
4
11
|
return if site.ssr?
|
|
5
12
|
|
|
6
|
-
|
|
7
|
-
|
|
13
|
+
config.collections.each_value do |collection_config|
|
|
14
|
+
next unless [:posts, :pages, :custom_collection].include?(collection_config.resource_type)
|
|
8
15
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
16
|
+
process_collection(
|
|
17
|
+
client: Client.new(
|
|
18
|
+
api_url: config.api_url,
|
|
19
|
+
token: config.token
|
|
20
|
+
),
|
|
21
|
+
collection_config: collection_config
|
|
22
|
+
)
|
|
23
|
+
end
|
|
12
24
|
end
|
|
13
25
|
|
|
14
26
|
private
|
|
15
27
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
28
|
+
# Determine the output directory for the given collection name
|
|
29
|
+
def collection_directory(collection_name)
|
|
30
|
+
case collection_name.to_s
|
|
31
|
+
when "posts"
|
|
32
|
+
File.join(site.source, "_posts")
|
|
33
|
+
when "pages"
|
|
34
|
+
File.join(site.source, "_pages")
|
|
35
|
+
else
|
|
36
|
+
File.join(site.source, "_#{collection_name}")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def build_directus_payload(item, collection_dir, collection_config, api_url = nil)
|
|
41
|
+
mapped_item = apply_data_mapping(item, collection_config)
|
|
42
|
+
slug = normalize_slug(mapped_item)
|
|
43
|
+
mapped_item["slug"] = slug
|
|
44
|
+
filename = build_filename(collection_dir, collection_config, mapped_item, slug)
|
|
45
|
+
mapped_item = transform_item_fields(mapped_item, api_url, collection_config.layout)
|
|
46
|
+
mapped_item["directus_generated"] = true # Add flag to front matter
|
|
47
|
+
content = mapped_item.delete("body") || ""
|
|
48
|
+
front_matter = generate_front_matter(mapped_item)
|
|
49
|
+
payload = render_markdown(front_matter, content)
|
|
50
|
+
[filename, payload]
|
|
19
51
|
end
|
|
20
52
|
|
|
21
|
-
def
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
53
|
+
def build_filename(collection_dir, collection_config, item, slug)
|
|
54
|
+
if collection_config.resource_type == :posts || collection_config.name.to_s == "posts"
|
|
55
|
+
post_date = extract_post_date(item)
|
|
56
|
+
if post_date
|
|
57
|
+
return File.join(collection_dir, "#{post_date.strftime("%Y-%m-%d")}-#{slug}.md")
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
File.join(collection_dir, "#{slug}.md")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def normalize_slug(item)
|
|
65
|
+
slug = item["slug"] || item[:slug]
|
|
66
|
+
slug = slug.to_s.strip
|
|
67
|
+
return slug unless slug.empty?
|
|
68
|
+
|
|
69
|
+
title = item["title"] || item[:title]
|
|
70
|
+
if title && defined?(Bridgetown::Utils) && Bridgetown::Utils.respond_to?(:slugify)
|
|
71
|
+
slug = Bridgetown::Utils.slugify(title.to_s)
|
|
27
72
|
else
|
|
28
|
-
|
|
73
|
+
slug = title.to_s.downcase.gsub(/[^a-z0-9]+/, "-").gsub(/^-|-$/, "")
|
|
29
74
|
end
|
|
30
75
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
76
|
+
slug = slug.strip
|
|
77
|
+
return slug unless slug.empty?
|
|
78
|
+
|
|
79
|
+
id = item["id"] || item[:id]
|
|
80
|
+
id.to_s
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def apply_data_mapping(item, collection_config)
|
|
84
|
+
mapped_item = item.dup
|
|
85
|
+
return mapped_item unless collection_config.fields.any? || collection_config.translations_enabled
|
|
86
|
+
|
|
87
|
+
mapped_fields = if collection_config.translations_enabled
|
|
88
|
+
DataMapper.map_translations(collection_config, item, resolve_locale)
|
|
89
|
+
else
|
|
90
|
+
DataMapper.map(collection_config, item)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
mapped_item.merge!(mapped_fields)
|
|
94
|
+
mapped_item
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def resolve_locale
|
|
98
|
+
return site.locale if site.respond_to?(:locale) && site.locale
|
|
99
|
+
|
|
100
|
+
config_locale = site.config["locale"] || site.config[:locale]
|
|
101
|
+
return config_locale.to_sym if config_locale
|
|
102
|
+
|
|
103
|
+
:en
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def extract_post_date(item)
|
|
107
|
+
raw = item["date"] || item[:date] || item["published_at"] || item[:published_at] || item["date_created"]
|
|
108
|
+
item["date"] ||= item["published_at"] if item["published_at"] && !item["date"]
|
|
109
|
+
return nil unless raw
|
|
110
|
+
|
|
111
|
+
return raw.to_date if raw.respond_to?(:to_date)
|
|
112
|
+
|
|
113
|
+
Date.parse(raw.to_s)
|
|
114
|
+
rescue ArgumentError
|
|
115
|
+
nil
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def transform_item_fields(item, api_url, layout)
|
|
119
|
+
item = item.dup
|
|
120
|
+
if item["image"] && api_url && !item["image"].to_s.start_with?("http://", "https://")
|
|
121
|
+
item["image"] = File.join(api_url, "assets", item["image"])
|
|
38
122
|
end
|
|
123
|
+
item["layout"] = layout if layout
|
|
124
|
+
item
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def generate_front_matter(item)
|
|
128
|
+
yaml = item.to_yaml
|
|
129
|
+
yaml.sub(%r{^---\s*\n}, "") # Remove leading --- if present
|
|
130
|
+
end
|
|
39
131
|
|
|
40
|
-
|
|
132
|
+
def render_markdown(front_matter, content)
|
|
133
|
+
"---\n#{front_matter}---\n\n#{content}"
|
|
41
134
|
end
|
|
42
135
|
|
|
43
|
-
def
|
|
44
|
-
|
|
136
|
+
def write_markdown_file(filename, payload)
|
|
137
|
+
FileUtils.mkdir_p(File.dirname(filename))
|
|
138
|
+
File.write(filename, payload)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def file_unchanged?(filename, payload)
|
|
142
|
+
return false unless File.exist?(filename)
|
|
143
|
+
|
|
144
|
+
File.read(filename) == payload
|
|
145
|
+
rescue StandardError
|
|
146
|
+
false
|
|
45
147
|
end
|
|
46
148
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
149
|
+
# Remove only plugin-generated Markdown files in the target directory before writing new ones
|
|
150
|
+
def clean_collection_directory(collection_dir, keep_files: [])
|
|
151
|
+
keep_set = keep_files.map { |file| File.expand_path(file) }.to_h { |file| [file, true] }
|
|
50
152
|
|
|
153
|
+
deleted = 0
|
|
154
|
+
Dir.glob(File.join(collection_dir, "*.md")).each do |file|
|
|
155
|
+
next if keep_set[File.expand_path(file)]
|
|
156
|
+
|
|
157
|
+
fm = File.read(file)[%r{\A---.*?---}m]
|
|
158
|
+
if fm && YAML.safe_load(fm, permitted_classes: [Date, Time, DateTime])["directus_generated"]
|
|
159
|
+
File.delete(file)
|
|
160
|
+
deleted += 1
|
|
161
|
+
end
|
|
162
|
+
rescue StandardError => e
|
|
163
|
+
warn "[BridgetownDirectus] Could not check/delete #{file}: #{e.message}"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
deleted
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def process_collection(client:, collection_config:)
|
|
170
|
+
endpoint = collection_config.endpoint || collection_config.name.to_s
|
|
51
171
|
begin
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
172
|
+
response = client.fetch_collection(endpoint, collection_config.default_query)
|
|
173
|
+
rescue StandardError => e
|
|
174
|
+
warn "Error fetching collection '#{endpoint}': #{e.message}"
|
|
175
|
+
return
|
|
176
|
+
end
|
|
177
|
+
collection_dir = collection_directory(collection_config.name)
|
|
178
|
+
FileUtils.mkdir_p(collection_dir)
|
|
179
|
+
api_url = site.config.bridgetown_directus.api_url
|
|
180
|
+
sanitized_response = sanitize_keys(response)
|
|
181
|
+
payloads = sanitized_response.to_h do |item|
|
|
182
|
+
build_directus_payload(item, collection_dir, collection_config, api_url)
|
|
183
|
+
end
|
|
184
|
+
log_directus("Generating #{collection_label(collection_config)} (#{payloads.size} items)")
|
|
185
|
+
deleted = clean_collection_directory(collection_dir, keep_files: payloads.keys)
|
|
186
|
+
written = 0
|
|
187
|
+
skipped = 0
|
|
188
|
+
payloads.each do |filename, payload|
|
|
189
|
+
if file_unchanged?(filename, payload)
|
|
190
|
+
skipped += 1
|
|
191
|
+
next
|
|
60
192
|
end
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
0
|
|
193
|
+
|
|
194
|
+
write_markdown_file(filename, payload)
|
|
195
|
+
written += 1
|
|
65
196
|
end
|
|
197
|
+
log_directus("Updated #{collection_label(collection_config)}: wrote #{written}, skipped #{skipped}, deleted #{deleted}")
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def log_directus(message)
|
|
201
|
+
return unless directus_logging_enabled?
|
|
202
|
+
|
|
203
|
+
Utils.log_directus(message)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def directus_logging_enabled?
|
|
207
|
+
flag = ENV["BRIDGETOWN_DIRECTUS_LOG"]
|
|
208
|
+
flag && !flag.to_s.strip.empty? && flag.to_s != "0"
|
|
66
209
|
end
|
|
67
210
|
|
|
68
|
-
def
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
layout "post"
|
|
84
|
-
title translation["title"]
|
|
85
|
-
content translation["body"]
|
|
86
|
-
date post["date"] || Time.now.iso8601
|
|
87
|
-
category post["category"]
|
|
88
|
-
excerpt translation["excerpt"]
|
|
89
|
-
image post["image"] ? "#{api_url}/assets/#{post['image']}" : nil
|
|
90
|
-
locale bridgetown_locale
|
|
91
|
-
translations translations
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
posts_created += 1
|
|
95
|
-
rescue => e
|
|
96
|
-
Utils.log_directus "Error creating post #{slug} for locale #{bridgetown_locale}: #{e.message}"
|
|
211
|
+
def collection_label(collection_config)
|
|
212
|
+
collection_config.name.to_s.tr("_", " ").split.map(&:capitalize).join(" ")
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Recursively sanitize keys to avoid illegal instance variable names (Ruby 3.4+)
|
|
216
|
+
def sanitize_keys(obj)
|
|
217
|
+
case obj
|
|
218
|
+
when Hash
|
|
219
|
+
obj.each_with_object({}) do |(k, v), h|
|
|
220
|
+
safe_key = if %r{^\d}.match?(k.to_s)
|
|
221
|
+
"n_#{k}"
|
|
222
|
+
else
|
|
223
|
+
k
|
|
224
|
+
end
|
|
225
|
+
h[safe_key] = sanitize_keys(v)
|
|
97
226
|
end
|
|
227
|
+
when Array
|
|
228
|
+
obj.map { |v| sanitize_keys(v) }
|
|
229
|
+
else
|
|
230
|
+
obj
|
|
98
231
|
end
|
|
232
|
+
end
|
|
99
233
|
|
|
100
|
-
|
|
234
|
+
# Recursively log all keys to find problematic ones
|
|
235
|
+
def log_all_keys(obj, path = "")
|
|
236
|
+
case obj
|
|
237
|
+
when Hash
|
|
238
|
+
obj.each do |k, v|
|
|
239
|
+
# puts "[BridgetownDirectus DEBUG] Key at #{path}: #{k.inspect}" if %r{^\d}.match?(k.to_s)
|
|
240
|
+
log_all_keys(v, "#{path}/#{k}")
|
|
241
|
+
end
|
|
242
|
+
when Array
|
|
243
|
+
obj.each_with_index do |v, idx|
|
|
244
|
+
log_all_keys(v, "#{path}[#{idx}]")
|
|
245
|
+
end
|
|
246
|
+
end
|
|
101
247
|
end
|
|
102
248
|
end
|
|
103
249
|
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "faraday"
|
|
5
|
+
|
|
6
|
+
module BridgetownDirectus
|
|
7
|
+
# Client for interacting with the Directus API
|
|
8
|
+
class Client
|
|
9
|
+
attr_reader :api_url, :token
|
|
10
|
+
|
|
11
|
+
def initialize(api_url:, token:)
|
|
12
|
+
@api_url = api_url
|
|
13
|
+
@token = token
|
|
14
|
+
return unless @token.nil? || @api_url.nil?
|
|
15
|
+
|
|
16
|
+
raise StandardError, "Invalid Directus configuration: missing API token or URL"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def fetch_collection(collection, params = {})
|
|
20
|
+
response = connection.get("/items/#{collection}") do |req|
|
|
21
|
+
req.params.merge!(prepare_params(params))
|
|
22
|
+
end
|
|
23
|
+
handle_response(response)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def fetch_item(collection, id, params = {})
|
|
27
|
+
response = connection.get("/items/#{collection}/#{id}") do |req|
|
|
28
|
+
req.params.merge!(prepare_params(params))
|
|
29
|
+
end
|
|
30
|
+
handle_response(response)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def fetch_items_with_filter(collection, filter, params = {})
|
|
34
|
+
merged_params = params.merge(filter: filter)
|
|
35
|
+
fetch_collection(collection, merged_params)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def fetch_related_items(collection, id, relation, params = {})
|
|
39
|
+
response = connection.get("/items/#{collection}/#{id}/#{relation}") do |req|
|
|
40
|
+
req.params.merge!(prepare_params(params))
|
|
41
|
+
end
|
|
42
|
+
handle_response(response)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def connection
|
|
48
|
+
@connection ||= Faraday.new(url: @api_url) do |faraday|
|
|
49
|
+
faraday.headers["Authorization"] = "Bearer #{@token}"
|
|
50
|
+
faraday.headers["Content-Type"] = "application/json"
|
|
51
|
+
faraday.adapter Faraday.default_adapter
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def prepare_params(params)
|
|
56
|
+
params.transform_keys(&:to_s)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def handle_response(response)
|
|
60
|
+
unless response.success?
|
|
61
|
+
Utils.log_directus "Directus API error: #{response.status} - #{response.body}"
|
|
62
|
+
raise "Directus API error: #{response.status}"
|
|
63
|
+
end
|
|
64
|
+
json = JSON.parse(response.body)
|
|
65
|
+
json["data"] || []
|
|
66
|
+
rescue JSON::ParserError => e
|
|
67
|
+
Utils.log_directus "Failed to parse Directus response: #{e.message}"
|
|
68
|
+
raise
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BridgetownDirectus
|
|
4
|
+
# Configuration module for Bridgetown Directus plugin
|
|
5
|
+
class Configuration
|
|
6
|
+
attr_reader :collections
|
|
7
|
+
attr_accessor :api_url, :token
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@collections = {}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Register a new collection with the given name
|
|
14
|
+
# @param name [Symbol] The name of the collection
|
|
15
|
+
# @param block [Proc] Configuration block for the collection
|
|
16
|
+
# @return [CollectionConfig] The collection configuration
|
|
17
|
+
def register_collection(name, &block)
|
|
18
|
+
collection = CollectionConfig.new(name)
|
|
19
|
+
collection.instance_eval(&block) if block_given?
|
|
20
|
+
@collections[name] = collection
|
|
21
|
+
collection
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Find a collection by name
|
|
25
|
+
# @param name [Symbol] The name of the collection
|
|
26
|
+
# @return [CollectionConfig, nil] The collection configuration or nil if not found
|
|
27
|
+
def find_collection(name)
|
|
28
|
+
@collections[name]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Collection configuration class
|
|
32
|
+
class CollectionConfig
|
|
33
|
+
attr_reader :name
|
|
34
|
+
|
|
35
|
+
# Initialize a new collection configuration
|
|
36
|
+
# @param name [Symbol] The name of the collection
|
|
37
|
+
def initialize(name)
|
|
38
|
+
@name = name
|
|
39
|
+
@fields = {}
|
|
40
|
+
@default_query = {}
|
|
41
|
+
@resource_type = :posts
|
|
42
|
+
@layout = "post"
|
|
43
|
+
@translations_enabled = false
|
|
44
|
+
@translatable_fields = []
|
|
45
|
+
@endpoint = nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Set up accessors for collection configuration properties
|
|
49
|
+
attr_accessor :endpoint, :fields, :default_query, :resource_type, :layout,
|
|
50
|
+
:translations_enabled, :translatable_fields
|
|
51
|
+
|
|
52
|
+
# Define a field mapping with optional converter
|
|
53
|
+
# @param bridgetown_field [Symbol] The field name in Bridgetown
|
|
54
|
+
# @param directus_field [String, Symbol] The field name in Directus
|
|
55
|
+
# @param converter [Proc, nil] Optional converter to transform the field value
|
|
56
|
+
# @return [void]
|
|
57
|
+
def field(bridgetown_field, directus_field, &converter)
|
|
58
|
+
@fields[bridgetown_field] = {
|
|
59
|
+
directus_field: directus_field.to_s,
|
|
60
|
+
converter: converter,
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Enable translations for this collection
|
|
65
|
+
# @param fields [Array<Symbol>] The fields that should be translated
|
|
66
|
+
# @return [void]
|
|
67
|
+
def enable_translations(fields = [])
|
|
68
|
+
@translations_enabled = true
|
|
69
|
+
@translatable_fields = fields
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Generate the resource path for a given item
|
|
73
|
+
# @param item [Hash] The data item from Directus
|
|
74
|
+
# @return [String] The resource path
|
|
75
|
+
def path(item)
|
|
76
|
+
# Default: /:resource_type/:slug/index.html
|
|
77
|
+
slug = item["slug"] || item[:slug] || item["id"] || item[:id]
|
|
78
|
+
"/#{resource_type}/#{slug}/index.html"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BridgetownDirectus
|
|
4
|
+
# Data mapper for transforming Directus data into Bridgetown resources
|
|
5
|
+
class DataMapper
|
|
6
|
+
class << self
|
|
7
|
+
# Map Directus data to Bridgetown format based on collection configuration
|
|
8
|
+
# @param collection_config [CollectionConfig] The collection configuration
|
|
9
|
+
# @param data [Hash] The Directus data
|
|
10
|
+
# @return [Hash] The mapped data
|
|
11
|
+
def map(collection_config, data)
|
|
12
|
+
mapped_data = {}
|
|
13
|
+
|
|
14
|
+
collection_config.fields.each do |bridgetown_field, field_config|
|
|
15
|
+
if field_config.is_a?(Hash)
|
|
16
|
+
directus_field = field_config[:directus_field]
|
|
17
|
+
converter = field_config[:converter]
|
|
18
|
+
|
|
19
|
+
value = extract_value(data, directus_field)
|
|
20
|
+
|
|
21
|
+
# Apply converter if provided
|
|
22
|
+
value = converter.call(value) if converter.respond_to?(:call)
|
|
23
|
+
|
|
24
|
+
mapped_data[bridgetown_field] = value
|
|
25
|
+
else
|
|
26
|
+
# Support for simple string mapping for backward compatibility
|
|
27
|
+
directus_field = field_config.to_s
|
|
28
|
+
mapped_data[bridgetown_field] = extract_value(data, directus_field)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
mapped_data
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Map translated fields from Directus data
|
|
36
|
+
# @param collection_config [CollectionConfig] The collection configuration
|
|
37
|
+
# @param data [Hash] The Directus data
|
|
38
|
+
# @param locale [Symbol] The locale to map
|
|
39
|
+
# @return [Hash] The mapped data with translations
|
|
40
|
+
def map_translations(collection_config, data, locale)
|
|
41
|
+
# First map the base data
|
|
42
|
+
mapped_data = map(collection_config, data)
|
|
43
|
+
|
|
44
|
+
# If translations are enabled and the data has translations
|
|
45
|
+
return mapped_data unless collection_config.translations_enabled && data["translations"]
|
|
46
|
+
|
|
47
|
+
# Find the translation for the requested locale
|
|
48
|
+
translation = find_translation_for_locale(data["translations"], locale)
|
|
49
|
+
|
|
50
|
+
# Apply translations if found
|
|
51
|
+
if translation
|
|
52
|
+
apply_translations(collection_config, translation, mapped_data)
|
|
53
|
+
mapped_data[:locale] = locale
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
mapped_data
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Resolve relationships in the data
|
|
60
|
+
# @param client [Client] The Directus client
|
|
61
|
+
# @param collection_config [CollectionConfig] The collection configuration
|
|
62
|
+
# @param data [Hash] The mapped data
|
|
63
|
+
# @param relationships [Hash] Relationship configuration
|
|
64
|
+
# @return [Hash] The data with resolved relationships
|
|
65
|
+
def resolve_relationships(client, collection_config, data, relationships)
|
|
66
|
+
return data unless relationships
|
|
67
|
+
|
|
68
|
+
resolved_data = data.dup
|
|
69
|
+
|
|
70
|
+
relationships.each do |field, relationship_config|
|
|
71
|
+
relation_id = data[field]
|
|
72
|
+
next unless relation_id
|
|
73
|
+
|
|
74
|
+
related_collection = relationship_config[:collection]
|
|
75
|
+
related_fields = relationship_config[:fields] || "*"
|
|
76
|
+
|
|
77
|
+
# Fetch the related item
|
|
78
|
+
related_item = client.fetch_item(
|
|
79
|
+
related_collection,
|
|
80
|
+
relation_id,
|
|
81
|
+
{ fields: related_fields }
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Add the related data to the resolved data
|
|
85
|
+
if related_item && related_item["data"]
|
|
86
|
+
resolved_data["#{field}_data"] = related_item["data"]
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
resolved_data
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
# Find translation for a specific locale
|
|
96
|
+
# @param translations [Array] Array of translation objects
|
|
97
|
+
# @param locale [Symbol] The locale to find
|
|
98
|
+
# @return [Hash, nil] The translation for the locale or nil if not found
|
|
99
|
+
def find_translation_for_locale(translations, locale)
|
|
100
|
+
translations.find do |t|
|
|
101
|
+
lang_code = t["languages_code"].to_s.split("-").first.downcase
|
|
102
|
+
lang_code == locale.to_s
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Apply translations to mapped data
|
|
107
|
+
# @param collection_config [CollectionConfig] The collection configuration
|
|
108
|
+
# @param translation [Hash] The translation data
|
|
109
|
+
# @param mapped_data [Hash] The mapped data to update
|
|
110
|
+
# @return [void]
|
|
111
|
+
def apply_translations(collection_config, translation, mapped_data)
|
|
112
|
+
collection_config.translatable_fields.each do |field|
|
|
113
|
+
# Get the Directus field name for this Bridgetown field
|
|
114
|
+
field_config = collection_config.fields[field]
|
|
115
|
+
|
|
116
|
+
directus_field = if field_config.nil?
|
|
117
|
+
field.to_s
|
|
118
|
+
elsif field_config.is_a?(Hash)
|
|
119
|
+
field_config[:directus_field]
|
|
120
|
+
else
|
|
121
|
+
field_config.to_s
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
directus_field = field.to_s if directus_field.to_s.empty?
|
|
125
|
+
|
|
126
|
+
# Check if the translation has this field
|
|
127
|
+
next unless translation[directus_field]
|
|
128
|
+
|
|
129
|
+
value = translation[directus_field]
|
|
130
|
+
|
|
131
|
+
# Apply converter if provided
|
|
132
|
+
if field_config.is_a?(Hash) && field_config[:converter].respond_to?(:call)
|
|
133
|
+
value = field_config[:converter].call(value)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
mapped_data[field] = value
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Extract a value from nested data using dot notation
|
|
141
|
+
# @param data [Hash] The data to extract from
|
|
142
|
+
# @param field [String] The field path (e.g., "user.profile.name")
|
|
143
|
+
# @return [Object] The extracted value
|
|
144
|
+
def extract_value(data, field)
|
|
145
|
+
return nil unless data
|
|
146
|
+
|
|
147
|
+
keys = field.to_s.split(".")
|
|
148
|
+
value = data
|
|
149
|
+
|
|
150
|
+
keys.each do |key|
|
|
151
|
+
return nil unless value.is_a?(Hash) && value.key?(key)
|
|
152
|
+
|
|
153
|
+
value = value[key]
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
value
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
module BridgetownDirectus
|
|
4
4
|
module Utils
|
|
5
5
|
def self.log_directus(message)
|
|
6
|
-
Bridgetown.logger
|
|
6
|
+
if defined?(Bridgetown) && Bridgetown.respond_to?(:logger)
|
|
7
|
+
Bridgetown.logger.info("Directus") { message }
|
|
8
|
+
elsif ENV["BRIDGETOWN_DIRECTUS_DEBUG"]
|
|
9
|
+
# Fallback for testing or when Bridgetown is not available
|
|
10
|
+
puts "[Directus] #{message}"
|
|
11
|
+
end
|
|
7
12
|
end
|
|
8
13
|
end
|
|
9
14
|
end
|