Skip to content

Commit becb870

Browse files
authored
feat: add --use_fragments option to resource generator | Closes #437 (#709)
Add a new `--use_fragments` (`-f`) flag to `mix ash_postgres.gen.resources` that generates attributes and relationships in a separate fragment file. This allows the fragment to be regenerated without affecting user customizations in the main resource file. When enabled: - Creates fragment at `{Resource}.Model` (e.g., `MyApp.Accounts.User.Model`) - Fragment contains attributes, relationships, and identities - Main resource includes `fragments: [FragmentModule]` option - If resource already exists, only regenerates the fragment * refactor: rename --use_fragments to --fragments and remove -f alias
1 parent 0ac6724 commit becb870

File tree

3 files changed

+390
-3
lines changed

3 files changed

+390
-3
lines changed

lib/mix/tasks/ash_postgres.gen.resources.ex

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ if Code.ensure_loaded?(Igniter) do
3434
- `public` - Mark all attributes and relationships as `public? true`. Defaults to `true`.
3535
- `no-migrations` - Do not generate snapshots & migrations for the resources. Defaults to `false`.
3636
- `skip-unknown` - Skip any attributes with types that we don't have a corresponding Elixir type for, and relationships that we can't assume the name of.
37+
- `fragments` - Generate attributes and relationships in a separate fragment file. This allows the fragment to be regenerated without affecting user customizations in the main resource file. Defaults to `false`.
3738
3839
## Tables
3940
@@ -61,7 +62,8 @@ if Code.ensure_loaded?(Igniter) do
6162
skip_unknown: :boolean,
6263
migrations: :boolean,
6364
snapshots_only: :boolean,
64-
domain: :keep
65+
domain: :keep,
66+
fragments: :boolean
6567
],
6668
aliases: [
6769
t: :tables,
@@ -74,7 +76,8 @@ if Code.ensure_loaded?(Igniter) do
7476
defaults: [
7577
default_actions: true,
7678
migrations: true,
77-
public: true
79+
public: true,
80+
fragments: false
7881
]
7982
}
8083
end

lib/resource_generator/resource_generator.ex

Lines changed: 146 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,11 @@ if Code.ensure_loaded?(Igniter) do
7676
|> Spec.add_relationships(resources, opts)
7777

7878
Enum.reduce(specs, igniter, fn table_spec, igniter ->
79-
table_to_resource(igniter, table_spec, domain, opts)
79+
if opts[:fragments] do
80+
table_to_resource_with_fragment(igniter, table_spec, domain, opts)
81+
else
82+
table_to_resource(igniter, table_spec, domain, opts)
83+
end
8084
end)
8185
end
8286

@@ -149,6 +153,147 @@ if Code.ensure_loaded?(Igniter) do
149153
end)
150154
end
151155

156+
defp table_to_resource_with_fragment(
157+
igniter,
158+
%AshPostgres.ResourceGenerator.Spec{} = table_spec,
159+
domain,
160+
opts
161+
) do
162+
fragment_module = Module.concat(table_spec.resource, Model)
163+
164+
fragment_content =
165+
"""
166+
use Spark.Dsl.Fragment,
167+
of: Ash.Resource
168+
169+
#{attributes_block(table_spec, opts)}
170+
#{identities_block(table_spec)}
171+
#{relationships_block(table_spec, opts)}
172+
"""
173+
174+
resource_path = Igniter.Project.Module.proper_location(igniter, table_spec.resource)
175+
176+
resource_exists? =
177+
Rewrite.has_source?(igniter.rewrite, resource_path) ||
178+
Igniter.exists?(igniter, resource_path)
179+
180+
if resource_exists? do
181+
# Only create/update the fragment file
182+
Igniter.Project.Module.create_module(igniter, fragment_module, fragment_content)
183+
else
184+
# Create both resource and fragment
185+
no_migrate_flag =
186+
if opts[:no_migrations] do
187+
"migrate? false"
188+
end
189+
190+
resource_content =
191+
"""
192+
use Ash.Resource,
193+
domain: #{inspect(domain)},
194+
data_layer: AshPostgres.DataLayer,
195+
fragments: [#{inspect(fragment_module)}]
196+
197+
#{default_actions(opts)}
198+
199+
postgres do
200+
table #{inspect(table_spec.table_name)}
201+
repo #{inspect(table_spec.repo)}
202+
#{schema_option(table_spec)}
203+
#{no_migrate_flag}
204+
#{references(table_spec, opts[:no_migrations])}
205+
#{custom_indexes(table_spec, opts[:no_migrations])}
206+
#{check_constraints(table_spec, opts[:no_migrations])}
207+
#{skip_unique_indexes(table_spec)}
208+
#{identity_index_names(table_spec)}
209+
end
210+
"""
211+
212+
igniter
213+
|> Igniter.Project.Module.create_module(fragment_module, fragment_content)
214+
|> Igniter.Project.Module.create_module(table_spec.resource, resource_content)
215+
|> Ash.Domain.Igniter.add_resource_reference(domain, table_spec.resource)
216+
|> then(fn igniter ->
217+
if opts[:extend] && opts[:extend] != [] do
218+
Igniter.compose_task(igniter, "ash.patch.extend", [
219+
table_spec.resource | opts[:extend] || []
220+
])
221+
else
222+
igniter
223+
end
224+
end)
225+
end
226+
end
227+
228+
defp attributes_block(table_spec, opts) do
229+
"""
230+
attributes do
231+
#{attributes(table_spec, opts)}
232+
end
233+
"""
234+
end
235+
236+
defp identities_block(%{indexes: indexes}) do
237+
indexes
238+
|> Enum.filter(fn %{unique?: unique?, columns: columns} ->
239+
unique? && Enum.all?(columns, &Regex.match?(~r/^[0-9a-zA-Z_]+$/, &1))
240+
end)
241+
|> Enum.map(fn index ->
242+
name = index.identity_name
243+
244+
fields = "[" <> Enum.map_join(index.columns, ", ", &":#{&1}") <> "]"
245+
246+
case identity_options(index) do
247+
"" ->
248+
"identity :#{name}, #{fields}"
249+
250+
options ->
251+
"""
252+
identity :#{name}, #{fields} do
253+
#{options}
254+
end
255+
"""
256+
end
257+
end)
258+
|> case do
259+
[] ->
260+
""
261+
262+
identities ->
263+
"""
264+
identities do
265+
#{Enum.join(identities, "\n")}
266+
end
267+
"""
268+
end
269+
end
270+
271+
defp relationships_block(%{relationships: []}, _opts), do: ""
272+
273+
defp relationships_block(%{relationships: relationships} = spec, opts) do
274+
relationships
275+
|> Enum.map_join("\n", fn relationship ->
276+
case relationship_options(spec, relationship, opts) do
277+
"" ->
278+
"#{relationship.type} :#{relationship.name}, #{inspect(relationship.destination)}"
279+
280+
options ->
281+
"""
282+
#{relationship.type} :#{relationship.name}, #{inspect(relationship.destination)} do
283+
#{options}
284+
end
285+
"""
286+
end
287+
end)
288+
|> then(fn rels ->
289+
"""
290+
relationships do
291+
#{rels}
292+
end
293+
"""
294+
end)
295+
end
296+
152297
defp schema_option(%{schema: schema}) when schema != "public" do
153298
"schema #{inspect(schema)}"
154299
end

0 commit comments

Comments
 (0)