Packages
ash
2.9.15
3.29.3
3.29.2
3.29.1
3.29.0
3.28.0
3.27.8
3.27.7
3.27.6
3.27.5
3.27.4
3.27.3
3.27.2
3.27.1
3.27.0
3.26.0
3.25.2
3.25.1
3.25.0
retired
3.24.7
3.24.6
3.24.5
3.24.4
3.24.3
3.24.2
3.24.1
3.24.0
3.23.1
3.23.0
3.22.2
3.22.1
3.22.0
3.21.3
3.21.2
3.21.1
3.21.0
3.20.0
3.19.3
3.19.2
3.19.1
3.19.0
3.18.0
3.17.1
3.17.0
3.16.0
3.15.0
3.14.1
3.14.0
retired
3.13.2
3.13.1
3.13.0
3.12.0
3.11.3
3.11.2
3.11.1
3.11.0
3.10.1
3.10.0
3.9.0
3.8.0
3.7.6
3.7.5
3.7.4
3.7.3
3.7.2
3.7.1
3.7.0
retired
3.6.3
retired
3.6.2
3.6.1
3.6.0
3.5.43
3.5.42
3.5.41
3.5.40
3.5.39
3.5.38
3.5.37
3.5.36
3.5.35
3.5.34
3.5.33
3.5.32
3.5.31
3.5.30
3.5.29
3.5.28
3.5.27
3.5.26
3.5.25
3.5.24
3.5.23
3.5.22
3.5.21
3.5.20
3.5.19
3.5.18
3.5.17
3.5.16
3.5.15
3.5.14
3.5.13
3.5.12
3.5.11
3.5.10
3.5.9
3.5.8
3.5.7
3.5.6
3.5.5
3.5.4
3.5.3
3.5.2
3.5.1
3.5.0
3.4.74
retired
3.4.73
3.4.72
3.4.71
3.4.70
3.4.69
3.4.68
3.4.67
3.4.66
3.4.65
3.4.64
3.4.63
3.4.62
3.4.61
3.4.60
3.4.59
3.4.58
3.4.57
3.4.56
3.4.55
3.4.54
3.4.53
3.4.52
3.4.51
3.4.50
3.4.49
3.4.48
3.4.47
3.4.46
3.4.45
3.4.44
3.4.43
3.4.42
3.4.41
3.4.40
3.4.39
3.4.38
3.4.37
3.4.36
3.4.35
3.4.34
3.4.33
3.4.32
3.4.31
3.4.30
3.4.29
3.4.28
3.4.27
3.4.26
3.4.25
3.4.24
3.4.23
3.4.22
3.4.21
3.4.20
3.4.19
3.4.18
3.4.17
3.4.16
3.4.15
3.4.14
3.4.13
3.4.12
3.4.11
3.4.10
3.4.9
3.4.8
3.4.7
3.4.6
3.4.5
3.4.4
3.4.3
3.4.2
3.4.1
3.4.0
3.3.3
3.3.2
3.3.1
3.3.0
3.2.6
3.2.5
3.2.4
3.2.3
3.2.2
3.2.1
3.2.0
3.1.8
3.1.7
3.1.6
3.1.5
3.1.4
3.1.3
3.1.2
3.1.1
3.1.0
3.0.16
3.0.15
3.0.14
3.0.13
3.0.12
3.0.11
3.0.10
3.0.9
3.0.8
3.0.7
3.0.6
3.0.5
3.0.4
3.0.3
3.0.2
3.0.1
3.0.0
3.0.0-rc.46
3.0.0-rc.45
3.0.0-rc.44
3.0.0-rc.43
3.0.0-rc.42
3.0.0-rc.41
3.0.0-rc.40
3.0.0-rc.39
3.0.0-rc.38
3.0.0-rc.37
3.0.0-rc.36
3.0.0-rc.35
3.0.0-rc.34
3.0.0-rc.33
3.0.0-rc.32
3.0.0-rc.31
3.0.0-rc.29
3.0.0-rc.28
3.0.0-rc.27
3.0.0-rc.26
3.0.0-rc.25
3.0.0-rc.24
3.0.0-rc.23
3.0.0-rc.22
3.0.0-rc.21
3.0.0-rc.20
3.0.0-rc.19
3.0.0-rc.18
3.0.0-rc.17
3.0.0-rc.16
3.0.0-rc.15
3.0.0-rc.14
3.0.0-rc.13
3.0.0-rc.12
3.0.0-rc.11
3.0.0-rc.10
3.0.0-rc.9
3.0.0-rc.8
3.0.0-rc.7
3.0.0-rc.6
3.0.0-rc.5
3.0.0-rc.4
3.0.0-rc.3
3.0.0-rc.1
3.0.0-rc.0
2.21.15
2.21.14
2.21.13
2.21.12
2.21.11
2.21.10
2.21.9
2.21.8
2.21.7
2.21.6
2.21.5
2.21.4
2.21.3
2.21.2
2.21.1
2.21.0
2.20.3
2.20.2
2.20.1
2.20.0
2.19.14
2.19.13
2.19.12
2.19.11
2.19.10
2.19.9
2.19.8
2.19.7
2.19.6
2.19.5
2.19.4
2.19.3
retired
2.19.2
retired
2.19.1
retired
2.19.0
retired
2.18.2
2.18.1
2.18.0
2.17.24
2.17.23
2.17.22
2.17.21
2.17.20
2.17.19
2.17.18
2.17.17
2.17.16
2.17.15
2.17.14
2.17.13
2.17.12
2.17.11
2.17.10
2.17.9
2.17.8
2.17.7
2.17.6
2.17.5
2.17.4
2.17.3
2.17.2
2.17.1
2.17.0
2.16.1
2.16.0
2.15.20
2.15.19
2.15.18
2.15.17
2.15.16
2.15.15
2.15.14
2.15.13
2.15.12
2.15.11
2.15.10
2.15.9
2.15.8
2.15.7
2.15.6
2.15.5
2.15.4
2.15.2
2.15.1
2.15.0
2.14.21
2.14.20
2.14.19
2.14.18
2.14.17
2.14.16
2.14.15
2.14.14
2.14.13
2.14.12
2.14.11
2.14.10
2.14.9
2.14.8
2.14.7
2.14.6
2.14.5
2.14.4
2.14.3
2.14.2
2.14.1
2.14.0
2.13.4
retired
2.13.3
2.13.2
2.13.1
2.13.0
2.12.1
2.12.0
2.11.11
2.11.10
2.11.9
2.11.8
2.11.7
2.11.6
2.11.5
2.11.4
2.11.3
2.11.2
2.11.1
2.11.0
2.11.0-rc.3
2.11.0-rc.2
2.11.0-rc.1
2.11.0-rc.0
2.10.2
2.10.1
2.10.0
2.9.29
2.9.28
2.9.27
2.9.26
2.9.25
2.9.24
2.9.23
2.9.22
2.9.21
2.9.20
2.9.19
2.9.18
2.9.17
2.9.16
2.9.15
2.9.14
2.9.13
2.9.12
2.9.11
2.9.10
2.9.9
2.9.8
2.9.7
2.9.6
2.9.5
2.9.4
2.9.3
2.9.2
2.9.1
2.9.0
2.8.1
2.8.0
2.7.1
2.7.0
2.6.31
2.6.30
2.6.29
2.6.28
2.6.27
2.6.26
2.6.25
2.6.24
2.6.23
2.6.22
2.6.21
2.6.20
2.6.19
2.6.18
2.6.17
2.6.16
2.6.15
2.6.14
2.6.13
2.6.11
2.6.10
2.6.9
2.6.8
2.6.7
2.6.6
2.6.5
2.6.4
2.6.3
2.6.2
2.6.1
2.6.0
2.5.16
2.5.15
2.5.14
2.5.13
2.5.12
2.5.11
2.5.10
2.5.9
2.5.8
2.5.7
2.5.6
2.5.5
2.5.4
2.5.3
2.5.2
2.5.1
2.5.0
2.5.0-rc.6
2.5.0-rc.5
2.5.0-rc.4
2.5.0-rc.3
2.5.0-rc.2
2.5.0-rc.1
2.5.0-rc.0
2.4.30
2.4.29
2.4.28
2.4.27
2.4.26
2.4.25
2.4.24
2.4.23
2.4.22
2.4.21
2.4.20
2.4.19
2.4.18
2.4.17
2.4.16
2.4.15
2.4.14
2.4.13
2.4.12
2.4.11
2.4.10
2.4.9
2.4.8
2.4.7
2.4.6
2.4.5
2.4.4
2.4.3
2.4.2
2.4.1
2.4.0
2.3.0
2.2.0
2.1.0
2.0.0
2.0.0-rc.15
2.0.0-rc.14
2.0.0-rc.13
2.0.0-rc.12
2.0.0-rc.11
2.0.0-rc.10
2.0.0-rc.9
2.0.0-rc.8
2.0.0-rc.7
2.0.0-rc.6
2.0.0-rc.5
2.0.0-rc.4
2.0.0-rc.3
2.0.0-rc.2
2.0.0-rc.1
2.0.0-rc.0
2.0.0-pre.8
2.0.0-pre.7
2.0.0-pre.6
2.0.0-pre.5
2.0.0-pre.4
2.0.0-pre.3
2.0.0-pre.2
2.0.0-pre.1
2.0.0-pre.0
1.53.3
1.53.2
1.53.0
1.52.0-rc.22
1.52.0-rc.21
1.52.0-rc.20
1.52.0-rc.19
1.52.0-rc.18
1.52.0-rc.17
1.52.0-rc.16
1.52.0-rc.15
1.52.0-rc.14
1.52.0-rc.13
1.52.0-rc.12
1.52.0-rc.11
1.52.0-rc.10
1.52.0-rc.9
1.52.0-rc.8
1.52.0-rc.7
1.52.0-rc.6
1.52.0-rc.5
1.52.0-rc.4
1.52.0-rc.3
1.52.0-rc.2
1.52.0-rc.1
1.52.0-rc.0
1.51.2
1.51.1
retired
1.51.0
1.50.21
1.50.20
1.50.19
1.50.18
1.50.17
1.50.16
1.50.15
1.50.14
1.50.13
1.50.12
1.50.11
1.50.10
1.50.9
1.50.8
1.50.7
1.50.6
1.50.5
1.50.4
1.50.3
1.50.2
1.50.1
1.50.0
1.49.0
1.48.0-rc.30
1.48.0-rc.29
1.48.0-rc.28
1.48.0-rc.27
1.48.0-rc.26
1.48.0-rc.25
1.48.0-rc.24
1.48.0-rc.23
1.48.0-rc.22
1.48.0-rc.21
1.48.0-rc.20
1.48.0-rc.19
1.48.0-rc.18
1.48.0-rc.17
1.48.0-rc.16
1.48.0-rc.15
1.48.0-rc.14
1.48.0-rc.13
1.48.0-rc.12
1.48.0-rc.11
1.48.0-rc.10
1.48.0-rc.9
1.48.0-rc.8
1.48.0-rc.7
1.48.0-rc.6
1.48.0-rc.5
1.48.0-rc.4
1.48.0-rc.3
1.48.0-rc.2
1.48.0-rc.1
1.48.0-rc.0
1.47.12
1.47.11
1.47.10
1.47.9
1.47.8
1.47.7
1.47.6
1.47.5
1.47.4
1.47.3
1.47.2
1.47.1
1.47.0
1.46.13
1.46.12
1.46.11
1.46.10
1.46.9
1.46.8
1.46.7
1.46.6
1.46.5
1.46.4
1.46.3
1.46.2
1.46.1
1.46.0
1.45.0-rc9
1.45.0-rc8
1.45.0-rc7
1.45.0-rc6
1.45.0-rc5
1.45.0-rc4
1.45.0-rc3
1.45.0-rc20
1.45.0-rc2
1.45.0-rc19
1.45.0-rc18
1.45.0-rc17
1.45.0-rc16
1.45.0-rc15
1.45.0-rc14
1.45.0-rc13
1.45.0-rc12
1.45.0-rc11
1.45.0-rc10
1.45.0-rc1
1.45.0-rc0
1.44.13
1.44.12
1.44.11
1.44.10
1.44.9
1.44.8
1.44.7
1.44.6
1.44.5
1.44.4
1.44.3
1.44.2
1.44.1
1.44.0
1.43.12
1.43.11
1.43.10
1.43.9
1.43.8
1.43.7
1.43.6
1.43.5
1.43.4
1.43.3
1.43.2
1.43.1
1.43.0
1.42.0
1.41.12
1.41.11
1.41.10
1.41.9
1.41.8
1.41.7
1.41.6
1.41.5
1.41.4
1.41.3
1.41.2
1.41.1
1.41.0
1.40.0
1.39.7
1.39.6
1.39.5
1.39.4
1.39.3
1.39.2
1.39.1
1.39.0
1.38.0
1.37.2
1.37.1
1.37.0
1.36.22
1.36.21
1.36.19
1.36.18
1.36.17
1.36.16
1.36.15
1.36.14
1.36.13
1.36.12
1.36.11
1.36.10
1.36.9
1.36.8
1.36.7
1.36.6
1.36.5
1.36.4
1.36.3
1.36.2
1.36.0
1.35.1
1.35.0
1.34.9
1.34.8
1.34.7
1.34.6
1.34.5
1.34.4
1.34.3
1.34.2
1.34.1
1.34.0
1.33.0
1.32.2
1.32.1
1.32.0
1.31.1
1.31.0
1.30.2
1.30.1
1.29.0-rc1
1.29.0-rc0
1.28.1
1.28.0
1.27.1
1.27.0
1.26.13
1.26.12
1.26.11
1.26.10
1.26.9
1.26.8
1.26.7
1.26.6
1.26.5
1.26.4
1.26.2
1.26.1
1.26.0
1.25.8
1.25.7
1.25.6
1.25.5
1.25.4
1.25.3
1.25.2
1.25.1
1.25.0
1.24.2
1.24.1
1.24.0
1.23.3
1.23.2
1.23.1
1.23.0
1.22.1
1.22.0
1.20.1
1.20.0
1.19.1
1.19.0
1.18.1
1.18.0
1.17.1
1.17.0
1.16.2
1.15.1
1.15.0
1.14.0
1.13.4
1.13.3
1.13.2
1.13.1
1.13.0
1.12.0
1.11.1
1.11.0
1.10.0
1.9.0
1.8.0
1.7.0
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.6.2
1.6.1
1.6.0
1.5.1
1.5.0
1.4.1
1.4.0
1.3.1
1.3.0
1.2.1
1.2.0
1.1.3
1.1.2
1.1.0
1.0.3
1.0.2
1.0.1
1.0.0
0.13.1
0.13.0
0.12.0
0.10.0
0.9.1
0.9.0
0.8.0
0.7.0
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.0
0.3.0
0.2.0
0.1.9
0.1.8
0.1.3
0.1.1
0.1.0
A declarative, extensible framework for building Elixir applications.
Security advisory:
This version has known vulnerabilities.
View advisories
Current section
Files
Jump to
Current section
Files
lib/ash/actions/create/bulk.ex
defmodule Ash.Actions.Create.Bulk do
@moduledoc false
@spec run(Ash.Api.t(), Ash.Resource.t(), atom(), Enumerable.t(map), Keyword.t()) ::
:ok
| {:ok, [Ash.Resource.record()]}
| {:ok, [Ash.Resource.record()], [Ash.Notifier.Notification.t()]}
| {:error, term}
def run(api, resource, action, inputs, opts) do
action = Ash.Resource.Info.action(resource, action)
if opts[:transaction] == :all && opts[:return_stream?] do
raise ArgumentError,
"Cannot specify `transaction: :all` and `return_stream?: true` together"
end
if opts[:return_stream?] && opts[:sorted?] do
raise ArgumentError, "Cannot specify `sorted?: true` and `return_stream?: true` together"
end
if opts[:transaction] == :all &&
Ash.DataLayer.data_layer_can?(resource, :transact) do
notify? =
if opts[:notify?] do
if Process.get(:ash_started_transaction?) do
false
else
Process.put(:ash_started_transaction?, true)
true
end
else
false
end
Ash.DataLayer.transaction(
List.wrap(resource) ++ action.touches_resources,
fn ->
do_run(api, resource, action, inputs, opts)
end,
opts[:timeout],
%{
type: :bulk_create,
metadata: %{
resource: resource,
action: action.name,
actor: opts[:actor]
}
}
)
|> case do
{:ok, bulk_result} ->
bulk_result =
if notify? do
%{
bulk_result
| notifications:
bulk_result.notifications ++ Process.delete(:ash_notifications) || []
}
else
bulk_result
end
handle_bulk_result(bulk_result, resource, action, opts)
{:error, error} ->
{:error, error}
end
else
api
|> do_run(resource, action, inputs, opts)
|> handle_bulk_result(resource, action, opts)
end
end
def do_run(api, resource, action, inputs, opts) do
opts = Ash.Actions.Helpers.set_opts(opts, api)
if action.manual? do
raise "Old style manual actions cannot be used with bulk creates"
end
upsert? = opts[:upsert?] || action.upsert?
upsert_fields = opts[:upsert_fields] || action.upsert_fields
if upsert? && !upsert_fields do
raise ArgumentError,
"For bulk actions, `upsert_fields` must be specified if upsert? is set to true`"
end
{_, opts} = Ash.Actions.Helpers.add_process_context(api, Ash.Changeset.new(resource), opts)
manual_action_can_bulk? =
case action.manual do
{mod, _opts} ->
function_exported?(mod, :bulk_create, 3)
_ ->
false
end
data_layer_can_bulk? = Ash.DataLayer.data_layer_can?(resource, :bulk_create)
batch_size =
if data_layer_can_bulk? || manual_action_can_bulk? do
opts[:batch_size] || 100
else
1
end
all_changes =
action.changes
|> Enum.concat(Ash.Resource.Info.validations(resource, action.type))
|> Enum.concat(Ash.Resource.Info.changes(resource, action.type))
|> Enum.with_index()
result =
inputs
|> Stream.with_index()
|> Stream.map(fn {input, index} ->
resource
|> Ash.Changeset.new()
|> Map.put(:api, api)
|> Ash.Changeset.set_context(%{
private: %{
upsert?: opts[:upsert?] || action.upsert? || false,
upsert_identity: opts[:upsert_identity] || action.upsert_identity,
upsert_fields: opts[:upsert_fields] || action.upsert_fields
}
})
|> Ash.Actions.Helpers.add_context(opts)
|> Ash.Changeset.set_context(%{bulk_create: %{index: index}})
|> Ash.Changeset.prepare_changeset_for_action(action, opts, input)
|> Ash.Changeset.run_before_transaction_hooks()
end)
|> transform_and_stop_on_errors(opts)
|> Stream.transform(
fn -> %{batch: [], count: 0, must_return_records?: opts[:notify?]} end,
fn
{:ok, item}, state when state.count < batch_size ->
must_return_records? = state.must_return_records? || !Enum.empty?(item.after_action)
{[],
%{
state
| batch: [item | state.batch],
count: state.count + 1,
must_return_records?: must_return_records?
}}
{:ok, item}, state ->
{[batch(state)], %{state | batch: [item], count: 0, must_return_records?: false}}
{:error, error}, state ->
{[{:error, error}], state}
end,
fn state ->
{[batch(state)], state}
end,
fn _ -> :ok end
)
|> Stream.map(fn
{:error, error} ->
{:error, error}
{:batch, batch_config} ->
%{count: count, batch: batch, must_return_records?: must_return_records?} = batch_config
batch =
Stream.map(batch, fn changeset ->
Ash.Changeset.set_defaults(changeset, :create, true)
end)
if opts[:transaction] == :batch &&
Ash.DataLayer.data_layer_can?(resource, :transact) do
notify? =
if opts[:notify?] do
if Process.get(:ash_started_transaction?) do
false
else
Process.put(:ash_started_transaction?, true)
true
end
else
false
end
try do
Ash.DataLayer.transaction(
List.wrap(resource) ++ (action.touches_resources || []),
fn ->
%{
must_return_records?: must_return_records_for_changes?,
batch: batch,
changes: changes
} =
batch
|> run_action_changes(
all_changes,
action,
opts[:actor],
opts[:authorize?],
opts[:tracer]
)
{batch, before_batch_notifications} =
batch
|> authorize(api, opts)
|> Enum.to_list()
|> run_bulk_before_batches(
changes,
all_changes,
opts
)
{batch, changesets_by_index} =
Enum.reduce(batch, {[], %{}}, fn changeset,
{changesets, changesets_by_index} ->
{[changeset | changesets],
Map.put(
changesets_by_index,
changeset.context.bulk_create.index,
changeset
)}
end)
run_batch(
resource,
batch,
action,
opts,
count,
changesets_by_index,
must_return_records?,
must_return_records_for_changes?,
data_layer_can_bulk?,
api,
before_batch_notifications
)
|> run_after_action_hooks(opts, api)
|> process_results(changes, all_changes, opts)
|> case do
{:error, error} ->
Ash.DataLayer.rollback(resource, error)
other ->
other
end
end,
opts[:timeout],
%{
type: :bulk_create,
metadata: %{
resource: resource,
action: action.name,
actor: opts[:actor]
}
}
)
|> case do
{:ok, result} ->
result
{:error, error} ->
{:error, error}
end
after
if notify? do
notifications = Process.get(:ash_notifications, [])
remaining_notifications = Ash.Notifier.notify(notifications)
Process.delete(:ash_notifications) || []
Ash.Actions.Helpers.warn_missed!(resource, action, %{
resource_notifications: remaining_notifications
})
end
end
else
%{
must_return_records?: must_return_records_for_changes?,
batch: batch,
changes: changes
} =
batch
|> run_action_changes(
all_changes,
action,
opts[:actor],
opts[:authorize?],
opts[:tracer]
)
{batch, before_batch_notifications} =
batch
|> authorize(api, opts)
|> Enum.to_list()
|> run_bulk_before_batches(
changes,
all_changes,
opts
)
{batch, changesets_by_index} =
Enum.reduce(batch, {[], %{}}, fn changeset, {changesets, changesets_by_index} ->
{[changeset | changesets],
Map.put(
changesets_by_index,
changeset.context.bulk_create.index,
changeset
)}
end)
run_batch(
resource,
batch,
action,
opts,
count,
changesets_by_index,
must_return_records?,
must_return_records_for_changes?,
data_layer_can_bulk?,
api,
before_batch_notifications
)
|> run_after_action_hooks(opts, api)
|> process_results(changes, all_changes, opts)
end
end)
if opts[:return_stream?] do
Stream.transform(
result,
fn -> nil end,
fn item, nil ->
case item do
{:error, error} ->
if opts[:stop_on_error?] do
{:halt, {[], error}}
else
{error_stream(error, opts), nil}
end
{:error, notifications, error} ->
if opts[:stop_on_error?] do
{:halt, {notify_stream([], notifications, resource, action, opts), error}}
else
{
notify_stream(error_stream(error, opts), notifications, resource, action, opts),
nil
}
end
{:ok, invalid, notifications} ->
if opts[:stop_on_error?] && !Enum.empty?(invalid) do
{:halt,
{[],
invalid
|> error_stream(opts)
|> notify_stream(notifications, resource, action, opts)}}
else
{invalid
|> error_stream(opts)
|> notify_stream(notifications, resource, action, opts), nil}
end
{:ok, batch_result, invalid, notifications} ->
if opts[:stop_on_error?] && !Enum.empty?(invalid) do
{:halt, {error_stream(invalid, opts), batch_result}}
else
if opts[:return_records?] do
{batch_result
|> Stream.map(&{:ok, &1})
|> Stream.concat(error_stream(invalid, opts))
|> notify_stream(notifications, resource, action, opts), nil}
else
{error_stream(invalid, opts)
|> notify_stream(notifications, resource, action, opts), nil}
end
end
:ok ->
{[], nil}
end
end,
fn
{errors, successes} ->
remaining_items =
successes
|> Stream.map(&{:ok, &1})
|> Stream.concat(error_stream(errors, opts))
{remaining_items, nil}
nil ->
{[], nil}
end,
& &1
)
else
result
|> Enum.reduce(%Ash.BulkResult{status: :empty, records: [], errors: []}, fn
{:error, error}, result ->
{error_count, errors} = errors(result, error, opts)
%{
result
| errors: errors,
error_count: error_count,
status: errored(result.status)
}
{:error, notifications, error}, result ->
{error_count, errors} = errors(result, error, opts)
%{
result
| errors: errors,
error_count: error_count,
status: errored(result.status),
notifications: (result.notifications || []) ++ notifications
}
{:ok, [], notifications}, result ->
%{
result
| notifications: (result.notifications || []) ++ notifications,
status: success(result.status)
}
{:ok, invalid, notifications}, result ->
{error_count, errors} = errors(result, invalid, opts)
%{
result
| errors: errors,
error_count: error_count,
status: :partial_success,
notifications: (result.notifications || []) ++ notifications
}
{:ok, batch_result, invalid, notifications}, result ->
status =
case invalid do
[] -> success(result.status)
_ -> :partial_success
end
records =
if opts[:return_records?] do
Enum.concat(Enum.to_list(batch_result), result.records)
else
result.records
end
{error_count, errors} = errors(result, invalid, opts)
%{
result
| records: records,
errors: errors,
error_count: error_count,
status: status,
notifications: (result.notifications || []) ++ notifications
}
:ok, result ->
result
end)
|> case do
%{status: :empty} = result ->
%{result | status: :success}
other ->
other
end
end
catch
{:error, error, batch_number, notifications} ->
status =
if batch_number > 1 do
:partial_success
else
:error
end
result = %Ash.BulkResult{
status: status,
notifications: notifications
}
{error_count, errors} = errors(result, error, opts)
%{result | errors: errors, error_count: error_count}
end
defp errors(result, invalid, opts) when is_list(invalid) do
Enum.reduce(invalid, {result.error_count, result.errors}, fn invalid, {error_count, errors} ->
errors(%{result | error_count: error_count, errors: errors}, invalid, opts)
end)
end
defp errors(result, {:error, error}, opts) do
if opts[:return_errors?] do
{result.error_count + 1, [error | result.errors]}
else
{result.error_count + 1, []}
end
end
defp errors(result, invalid, opts) do
if Enumerable.impl_for(invalid) do
invalid = Enum.to_list(invalid)
errors(result, invalid, opts)
else
errors(result, {:error, invalid}, opts)
end
end
defp error_stream(error, opts) do
if opts[:return_errors?] do
error
|> List.wrap()
|> Stream.map(&{:error, &1})
else
[]
end
end
defp run_bulk_before_batches(
batch,
changes,
all_changes,
opts
) do
all_changes
|> Enum.with_index()
|> Enum.filter(fn
{%{change: {module, _opts}}, _} ->
function_exported?(module, :before_batch, 3)
_ ->
false
end)
|> Enum.reduce(batch, fn {%{change: {module, change_opts}}, index}, batch ->
{matches, non_matches} =
batch
|> Enum.split_with(fn
%{valid?: false} ->
false
changeset ->
changes[index] == :all or
changeset.context.bulk_create.index in List.wrap(changes[index])
end)
before_batch_results =
module.before_batch(matches, change_opts, %{
actor: opts[:actor],
tracer: opts[:tracer],
authorize?: opts[:authorize?]
})
Enum.concat([before_batch_results, non_matches])
end)
|> Enum.reduce(
{[], []},
fn
%Ash.Notifier.Notification{} = notification, {changesets, notifications} ->
{changesets, [notification | notifications]}
result, {changesets, notifications} ->
{[result | changesets], notifications}
end
)
end
defp notify_stream(stream, notifications, resource, action, opts) do
if opts[:notify?] do
notifications = List.wrap(notifications)
if opts[:return_notifications?] do
Stream.concat(stream, Stream.map(notifications, &{:notification, &1}))
else
remaining = Ash.Notifier.notify(notifications)
Ash.Actions.Helpers.warn_missed!(resource, action, %{
resource_notifications: remaining
})
stream
end
else
stream
end
end
defp errored(:empty), do: :error
defp errored(:partial_success), do: :partial_success
defp errored(:success), do: :partial_success
defp errored(:error), do: :error
defp success(:empty), do: :success
defp success(:partial_success), do: :partial_success
defp success(:error), do: :partial_success
defp success(:success), do: :success
defp transform_and_stop_on_errors(stream, opts) do
Stream.map(stream, fn changeset ->
if changeset.valid? do
{:ok, changeset}
else
if opts[:stop_on_error?] && !opts[:return_stream?] do
throw({:error, Ash.Error.to_error_class(changeset.errors), 0, []})
else
{:error, changeset}
end
end
end)
end
defp authorize(batch, api, opts) do
if opts[:authorize?] do
batch
|> Stream.map(fn changeset ->
if changeset.valid? do
case api.can(changeset, opts[:actor], return_forbidden_error?: true, maybe_is: false) do
{:ok, true} ->
changeset
{:ok, false, error} ->
Ash.Changeset.add_error(changeset, error)
{:error, error} ->
Ash.Changeset.add_error(changeset, error)
end
else
changeset
end
end)
else
batch
end
end
defp handle_bulk_result(%Ash.BulkResult{} = bulk_result, resource, action, opts) do
bulk_result
|> notify(resource, action, opts)
|> sort(opts)
end
# for when we return a stream
defp handle_bulk_result(stream, _, _, _), do: stream
defp sort(%{records: records} = result, opts) when is_list(records) do
if opts[:sorted?] do
%{result | records: Enum.sort_by(records, & &1.__metadata__.bulk_create_index)}
else
result
end
end
defp sort(result, _), do: result
defp notify(%{notifications: []} = result, _resource, _action, _opts), do: result
defp notify(%{notifications: notifications} = result, resource, action, opts) do
if opts[:return_notifications?] do
result
else
if opts[:notify?] do
result = %{result | notifications: Ash.Notifier.notify(notifications)}
Ash.Actions.Helpers.warn_missed!(resource, action, %{
resource_notifications: result.notifications
})
result
else
result
end
end
end
defp run_batch(
resource,
batch,
action,
opts,
count,
changesets_by_index,
must_return_records?,
must_return_records_for_changes?,
data_layer_can_bulk?,
api,
before_batch_notifications
) do
{batch, invalid, notifications} =
batch
|> Stream.map(fn changeset ->
Ash.Changeset.require_values(
changeset,
:create
)
|> Ash.Changeset.require_values(
:update,
false,
action.require_attributes
)
end)
|> Enum.reduce({[], [], before_batch_notifications}, fn changeset,
{changesets, invalid, notifications} ->
if changeset.valid? do
{changeset, %{notifications: new_notifications}} =
Ash.Changeset.run_before_actions(changeset)
new_notifications =
if opts[:notify?] do
new_notifications
else
[]
end
{changeset, manage_notifications} =
if changeset.valid? do
case Ash.Actions.ManagedRelationships.setup_managed_belongs_to_relationships(
changeset,
opts[:actor],
authorize?: opts[:authorize?],
actor: opts[:actor]
) do
{:error, error} ->
{Ash.Changeset.add_error(changeset, error), new_notifications}
{changeset, manage_instructions} ->
{changeset, manage_instructions.notifications}
end
else
{changeset, []}
end
if changeset.valid? do
{[changeset | changesets], invalid,
notifications ++ new_notifications ++ manage_notifications}
else
if opts[:stop_on_error?] && !opts[:return_stream?] do
throw({:error, Ash.Error.to_error_class(changeset.errors), 0, []})
end
{changesets, [changeset | invalid], notifications ++ new_notifications}
end
else
{changesets, [changeset | invalid], notifications}
end
end)
case batch do
[] ->
{:ok, [], invalid, notifications, changesets_by_index}
batch ->
upsert_keys =
if opts[:upsert?] do
case opts[:upsert_identity] || action.upsert_identity do
nil ->
Ash.Resource.Info.primary_key(resource)
identity ->
keys =
resource
|> Ash.Resource.Info.identities()
|> Enum.find(&(&1.name == identity))
|> Kernel.||(
raise ArgumentError,
"No identity found for #{inspect(resource)} called #{inspect(identity)}"
)
|> Map.get(:keys)
if opts[:tenant] &&
Ash.Resource.Info.multitenancy_strategy(resource) == :attribute do
[Ash.Resource.Info.multitenancy_attribute(resource) | keys]
else
keys
end
end
end
changesets_by_index =
Enum.reduce(batch, changesets_by_index, fn changeset, changesets_by_index ->
Map.put(changesets_by_index, changeset.context.bulk_create.index, changeset)
end)
case action.manual do
{mod, opts} ->
if function_exported?(mod, :bulk_create, 3) do
mod.bulk_create(batch, opts, %{
actor: opts[:actor],
authorize?: opts[:authorize?],
tracer: opts[:tracer],
api: api,
batch_size: count,
upsert?: opts[:upsert?] || action.upsert?,
upsert_keys: upsert_keys,
upsert_fields: opts[:upsert_fields] || action.upsert_fields,
return_records?:
opts[:return_records?] || must_return_records? ||
must_return_records_for_changes?,
tenant: opts[:tenant]
})
else
[changeset] = batch
result =
mod.create(changeset, opts, %{
actor: opts[:actor],
tenant: opts[:tenant],
authorize?: opts[:authorize?],
tracer: opts[:tracer],
api: api
})
case result do
{:ok, result} ->
{:ok,
[
Ash.Resource.put_metadata(
result,
:bulk_create_index,
changeset.context.bulk_create.index
)
]}
{:error, error} ->
{:error, error}
end
end
_ ->
if data_layer_can_bulk? do
Ash.DataLayer.bulk_create(resource, batch, %{
batch_size: count,
return_records?:
opts[:return_records?] || must_return_records? ||
must_return_records_for_changes?,
upsert?: opts[:upsert?] || action.upsert? || false,
upsert_keys: upsert_keys,
upsert_fields: opts[:upsert_fields] || action.upsert_fields,
tenant: opts[:tenant]
})
else
[changeset] = batch
upsert? = opts[:upsert?] || action.upsert? || false
result =
if upsert? do
Ash.DataLayer.upsert(resource, changeset, upsert_keys)
else
Ash.DataLayer.create(resource, changeset)
end
case result do
{:ok, result} ->
{:ok,
[
Ash.Resource.put_metadata(
result,
:bulk_create_index,
changeset.context.bulk_create.index
)
]}
{:error, error} ->
{:error, error}
end
end
end
|> case do
{:ok, result} ->
{:ok, result, invalid, notifications, changesets_by_index}
:ok ->
{:ok, invalid, notifications, changesets_by_index}
other ->
other
end
end
end
defp manage_relationships(created, api, changeset, engine_opts) do
with {:ok, loaded} <-
Ash.Actions.ManagedRelationships.load(api, created, changeset, engine_opts),
{:ok, with_relationships, new_notifications} <-
Ash.Actions.ManagedRelationships.manage_relationships(
loaded,
changeset,
engine_opts[:actor],
engine_opts
) do
{:ok, with_relationships, %{notifications: new_notifications, new_changeset: changeset}}
end
end
defp run_after_action_hooks({:ok, invalid, notifications, changesets_by_index}, _opts, _api) do
{:ok, invalid, notifications, changesets_by_index}
end
defp run_after_action_hooks(
{:ok, batch_results, invalid, notifications, changesets_by_index},
opts,
api
) do
batch_results
|> Enum.reduce_while(
{:ok, [], notifications, changesets_by_index},
fn result, {:ok, records, notifications, changesets_by_index} ->
changeset = changesets_by_index[result.__metadata__.bulk_create_index]
case manage_relationships(result, api, changeset,
actor: opts[:actor],
authorize?: opts[:authorize?]
) do
{:ok, result, %{notifications: new_notifications, new_changeset: changeset}} ->
case Ash.Changeset.run_after_actions(result, changeset, []) do
{:error, error} ->
{:halt, {:error, error}}
{:ok, result, changeset, %{notifications: more_new_notifications}} ->
{:cont,
{:ok, [result | records],
notifications ++ new_notifications ++ more_new_notifications,
Map.put(changesets_by_index, result.__metadata__.bulk_create_index, changeset)}}
end
{:error, error} ->
{:error, error}
end
end
)
|> case do
{:ok, results, notifications, changesets_by_index} ->
{:ok, results, invalid, notifications, changesets_by_index}
other ->
other
end
end
defp run_after_action_hooks({:error, error}, _opts, _api) do
{:error, error}
end
defp process_results({:ok, invalid, notifications, _changesets_by_index}, _, _, _),
do: {:ok, invalid, notifications}
defp process_results(
{:ok, batch, invalid, notifications, changesets_by_index},
changes,
all_changes,
opts
) do
Enum.reduce(
batch,
{[], notifications, changesets_by_index, []},
fn result, {results, notifications, changesets_by_index, errors} ->
changeset = changesets_by_index[result.__metadata__.bulk_create_index]
notifications =
if opts[:notify?] do
[notification(changeset, result, opts) | notifications]
else
notifications
end
try do
case Ash.Changeset.run_after_transactions({:ok, result}, changeset) do
{:ok, result} ->
{[result | results], notifications, changesets_by_index, errors}
{:error, error} ->
{results, notifications, changesets_by_index, [error | errors]}
end
rescue
e ->
{results, notifications, changesets_by_index, [e | errors]}
end
end
)
|> case do
{results, notifications, changesets_by_index, []} ->
case run_bulk_after_changes(changes, all_changes, results, changesets_by_index, opts) do
{results, new_notifications, []} ->
{:ok, results, invalid, new_notifications ++ notifications}
{_results, _new_notifications, errors} ->
{:error, notifications, Ash.Error.to_ash_error(errors)}
end
{_result, notifications, _, errors} ->
{:error, notifications, Ash.Error.to_ash_error(errors)}
end
end
defp process_results({:error, error}, _, _, _), do: {:error, error}
defp run_bulk_after_changes(changes, all_changes, results, changesets_by_index, opts) do
results =
Stream.map(results, fn result ->
{:ok, result}
end)
all_changes
|> Enum.with_index()
|> Enum.filter(fn
{%{change: {module, _opts}}, _} ->
function_exported?(module, :after_batch, 3)
_ ->
false
end)
|> Enum.reduce(results, fn {%{change: {module, change_opts}}, index}, results ->
{matches, non_matches} =
results
|> Enum.split_with(fn
{:ok, result} ->
changes[index] == :all or
result.__metadata__.bulk_create_index in List.wrap(changes[index])
_ ->
false
end)
matches =
Enum.map(matches, fn match ->
{changesets_by_index[match.__metadata__.bulk_create_index], match}
end)
after_batch_results =
module.after_batch(matches, change_opts, %{
actor: opts[:actor],
tracer: opts[:tracer],
authorize?: opts[:authorize?]
})
Enum.concat([after_batch_results, non_matches])
end)
|> Enum.reduce(
{[], [], []},
fn
%Ash.Notifier.Notification{} = notification, {results, notifications, errors} ->
{results, [notification | notifications], errors}
{:ok, result}, {results, notifications, errors} ->
{[result | results], notifications, errors}
{:error, error}, {results, notifications, errors} ->
{results, notifications, [error | errors]}
end
)
end
defp notification(changeset, result, opts) do
%Ash.Notifier.Notification{
resource: changeset.resource,
api: changeset.api,
actor: opts[:actor],
action: changeset.action,
data: result,
changeset: changeset
}
end
defp run_action_changes(batch, all_changes, _action, actor, authorize?, tracer) do
Enum.reduce(
all_changes,
%{must_return_records?: false, batch: batch, changes: %{}, notifications: []},
fn
{%{validation: {module, opts}} = validation, _change_index}, %{batch: batch} = state ->
batch =
Stream.map(batch, fn changeset ->
if Enum.all?(validation.where || [], fn {module, opts} ->
opts =
Ash.Filter.build_filter_from_template(
opts,
actor,
changeset.arguments,
changeset.context
)
module.validate(changeset, opts) == :ok
end) do
case module.validate(changeset, opts) do
:ok ->
changeset
{:error, error} ->
Ash.Changeset.add_error(changeset, error)
end
else
changeset
end
end)
%{
must_return_records?: state.must_return_records?,
batch: batch,
changes: state.changes
}
{%{change: {module, change_opts}} = change, change_index}, state ->
if Enum.empty?(change.where) && !change.only_when_valid? do
context = %{
actor: actor,
authorize?: authorize? || false,
tracer: tracer
}
batch = batch_change(module, batch, change_opts, context, actor)
must_return_records? =
state.must_return_records? ||
Enum.any?(batch, fn item ->
item.relationships not in [nil, %{}]
end) || function_exported?(module, :after_batch, 3)
%{
must_return_records?: must_return_records?,
batch: batch,
changes: Map.put(state.changes, change_index, :all)
}
else
{matches, non_matches} =
batch
|> Enum.split_with(fn changeset ->
applies_from_where? =
Enum.all?(change.where || [], fn {module, opts} ->
opts =
Ash.Filter.build_filter_from_template(
opts,
actor,
changeset.arguments,
changeset.context
)
module.validate(changeset, opts) == :ok
end)
applies_from_only_when_valid? =
if change.only_when_valid? do
changeset.valid?
else
true
end
applies_from_where? and applies_from_only_when_valid?
end)
if Enum.empty?(matches) do
%{
must_return_records?: state.must_return_records?,
batch: non_matches,
changes: state.changes
}
else
context = %{
actor: actor,
authorize?: authorize? || false,
tracer: tracer
}
matches = batch_change(module, matches, change_opts, context, actor)
must_return_records? =
state.must_return_records? || function_exported?(module, :after_batch, 3)
%{
must_return_records?: must_return_records?,
batch: Enum.concat(matches, non_matches),
changes:
Map.put(
state.changes,
change_index,
Enum.map(matches, & &1.context.bulk_create.index)
)
}
end
end
end
)
end
defp batch_change(module, batch, change_opts, context, actor) do
built_change_opts =
Ash.Filter.build_filter_from_template(
change_opts,
actor,
%{},
context
)
# TODO: We should figure out how to remove this requirement
# the basic problem is that if someone writes `set_attribute(:foo, ^arg(:arg))`
# we can't use `.batch_change/3` (because which argument value do we pass in?)
if function_exported?(module, :batch_change, 3) && built_change_opts == change_opts do
module.batch_change(batch, change_opts, context)
else
Enum.map(batch, fn changeset ->
change_opts =
Ash.Filter.build_filter_from_template(
change_opts,
actor,
changeset.arguments,
changeset.context
)
module.change(changeset, change_opts, Map.put(context, :bulk?, true))
end)
end
end
defp batch(state) do
{:batch,
%{
count: state.count,
batch: state.batch,
must_return_records?: state.must_return_records?
}}
end
end