Lightning.Projects (Lightning v2.14.5-pre1)

View Source

The Projects context.

Summary

Functions

Appends a new 12-hex head hash to project.version_history (append-only).

Like append_project_head/2, but raises on invalid input and performs the append within a transaction that locks the project row.

Returns an %Ecto.Changeset{} for tracking project changes.

Computes a deterministic 12-hex “project head” hash from the latest version hash per workflow.

Creates a sandbox under the given parent by delegating to create_project/2.

Deletes a project and its related data, including workflows, work orders, steps, jobs, runs, triggers, project users, project credentials, and dataclips

Deletes project dataclips in batches

Deletes a project user and removes their credentials from the project.

Deletes project work orders in batches

Fetches a project by id (root or sandbox) and preloads its direct :parent.

Fetches a project by id (root or sandbox) and preloads its direct :parent.

Gets a single project_user.

Returns the role of a user in a project. Possible roles are :admin, :viewer, :editor, and :owner

Get all project users for a given project

Gets a single project with it's members via project_users.

Fetches projects for a given user from the database.

Returns a read-only “workspace” view for a parent project: the parent itself plus all of its direct sandboxes (unique set, no preloads).

Lists emails of users with :owner or :admin roles in the project

Returns the list of projects.

Lists all projects that have history retention

Returns the direct sandboxes (children) of a parent project, ordered by name (ASC).

Perform, when called with %{"type" => "purge_deleted"} will find projects that are ready for permanent deletion and purge them.

Builds a query to retrieve projects associated with a user.

Provisions a sandbox (child) project under the given parent.

Returns the root ancestor of a project by walking up parent_id links.

Should input or output dataclips be saved for runs in this project?

Given a project, this function sets a scheduled deletion date based on the PURGE_DELETED_AFTER_DAYS environment variable. If no ENV is set, this date defaults to NOW but the automatic project purge cronjob will never run. (Note that subsequent logins will be blocked for projects pending deletion.)

Returns an %Ecto.Changeset{} for changing the project scheduled_deletion.

Functions

add_project_users(project, project_users, notify_users \\ true)

@spec add_project_users(Lightning.Projects.Project.t(), [map(), ...], boolean()) ::
  {:ok, [Lightning.Projects.ProjectUser.t(), ...]}
  | {:error, Ecto.Changeset.t()}

append_project_head(project, hash)

@spec append_project_head(Lightning.Projects.Project.t(), String.t()) ::
  {:ok, Lightning.Projects.Project.t()} | {:error, :bad_hash}

Appends a new 12-hex head hash to project.version_history (append-only).

This is a lenient wrapper that validates using Lightning.Validators.Hex.valid?(hash) and returns tagged tuples. For atomic locking and errors by exception, use append_project_head!/2.

Validation

  • hash must be 12 lowercase hex characters (0-9, a-f).
  • No duplicates: if the hash already exists in the array, this is a no-op.

Concurrency

The underlying !/2 variant locks the row (FOR UPDATE) to avoid lost updates.

append_project_head!(project, hash)

Like append_project_head/2, but raises on invalid input and performs the append within a transaction that locks the project row.

Behavior

  • Raises ArgumentError if hash is not 12 lowercase hex.
  • Uses SELECT … FOR UPDATE to read the current array, appends if missing, and writes back in the same transaction.
  • Idempotent: if the hash is already present, returns the unchanged project.

cancel_scheduled_deletion(project_id)

change_project(project, attrs \\ %{})

Returns an %Ecto.Changeset{} for tracking project changes.

Examples

iex> change_project(project)
%Ecto.Changeset{data: %Project{}}

compute_project_head_hash(project_id)

Computes a deterministic 12-hex “project head” hash from the latest version hash per workflow.

The algorithm:

  1. For each workflow in the project, select the most recent row in workflow_versions by (inserted_at DESC, id DESC).
  2. Build pairs [[workflow_id_as_string, hash], ...].
  3. JSON-encode the pairs and take sha256 of the bytes.
  4. Return the first 12 lowercase hex chars.

Guarantees

  • Deterministic for a given set of latest heads.
  • If a project has no workflow versions, returns the digest of [], i.e. a stable 12-hex string representing “empty”.

Use cases

  • Change detection across environments.
  • Cache keys and optimistic comparisons (e.g. “is this workspace up-to-date?”).

create_project(attrs \\ %{}, schedule_email? \\ true)

Creates a project.

Examples

iex> create_project(%{field: value})
{:ok, %Project{}}

iex> create_project(%{field: bad_value})
{:error, %Ecto.Changeset{}}

create_sandbox(project, attrs, schedule_email? \\ false)

@spec create_sandbox(Lightning.Projects.Project.t(), map(), boolean()) ::
  {:ok, Lightning.Projects.Project.t()} | {:error, Ecto.Changeset.t()}

Creates a sandbox under the given parent by delegating to create_project/2.

This is a convenience wrapper that sets :parent_id and preserves the existing behavior around collaborator emails (off by default unless schedule_email? is true).

Notes

  • Child names are scoped-unique by (parent_id, name). Root names may repeat, but two siblings cannot share a name (enforced by the projects_unique_child_name index).
  • This function does not clone workflows, credentials, or dataclips. It only creates a new project row with parent_id set. See sandbox provisioning flow for full cloning.

Returns

  • {:ok, %Project{}} on success
  • {:error, %Ecto.Changeset{}} on validation/unique errors

delete_project(project)

Deletes a project and its related data, including workflows, work orders, steps, jobs, runs, triggers, project users, project credentials, and dataclips

Examples

iex> delete_project(project)
{:ok, %Project{}}

iex> delete_project(project)
{:error, %Ecto.Changeset{}}

delete_project_async(project)

@spec delete_project_async(Lightning.Projects.Project.t()) :: {:ok, Oban.Job.t()}

delete_project_dataclips(project, batch_size \\ 1000)

@spec delete_project_dataclips(Lightning.Projects.Project.t(), non_neg_integer()) ::
  :ok

Deletes project dataclips in batches

delete_project_user!(project_user)

Deletes a project user and removes their credentials from the project.

This function:

  1. Deletes the association between the user and the project
  2. Removes any credentials owned by the user from the project

All operations are performed within a transaction for data consistency.

Parameters

  • project_user: The ProjectUser struct to be deleted

Returns

  • The deleted ProjectUser struct

delete_project_workorders(project, batch_size \\ 1000)

@spec delete_project_workorders(Lightning.Projects.Project.t(), non_neg_integer()) ::
  :ok

Deletes project work orders in batches

export_project(atom, project_id, snapshot_ids \\ nil)

@spec export_project(atom(), Ecto.UUID.t(), [Ecto.UUID.t()] | nil) :: {:ok, binary()}

Exports a project as yaml.

Examples

iex> export_project(:yaml, project_id)
{:ok, string}

find_users_to_notify_of_trigger_failure(project_id)

get_project(id)

@spec get_project(Ecto.UUID.t()) :: Lightning.Projects.Project.t() | nil

Fetches a project by id (root or sandbox) and preloads its direct :parent.

Returns nil if no project with the given id exists.

get_project!(id)

@spec get_project!(Ecto.UUID.t()) :: Lightning.Projects.Project.t()

Fetches a project by id (root or sandbox) and preloads its direct :parent.

Raises Ecto.NoResultsError if no project with the given id exists.

get_project_credential(project_id, credential_id)

get_project_user(id)

@spec get_project_user(Ecto.UUID.t()) :: Lightning.Projects.ProjectUser.t() | nil

get_project_user(project_id, user)

@spec get_project_user(
  project :: Lightning.Projects.Project.t(),
  user :: Lightning.Accounts.User.t()
) ::
  Lightning.Projects.ProjectUser.t() | nil
@spec get_project_user(project_id :: binary(), user :: Lightning.Accounts.User.t()) ::
  Lightning.Projects.ProjectUser.t() | nil

get_project_user!(id)

Gets a single project_user.

Raises Ecto.NoResultsError if the ProjectUser does not exist.

Examples

iex> get_project_user!(123)
%ProjectUser{}

iex> get_project_user!(456)
** (Ecto.NoResultsError)

get_project_user_role(user, project)

@spec get_project_user_role(
  user :: Lightning.Accounts.User.t(),
  project :: Lightning.Projects.Project.t()
) :: atom() | nil

Returns the role of a user in a project. Possible roles are :admin, :viewer, :editor, and :owner

Examples

iex> get_project_user_role(user, project)
:admin

iex> get_project_user_role(user, project)
:viewer

iex> get_project_user_role(user, project)
:editor

iex> get_project_user_role(user, project)
:owner

get_project_users!(id)

Get all project users for a given project

get_project_with_users!(id)

Gets a single project with it's members via project_users.

Raises Ecto.NoResultsError if the Project does not exist.

Examples

iex> get_project!(123)
%Project{}

iex> get_project!(456)
** (Ecto.NoResultsError)

get_projects_for_user(user)

@spec get_projects_for_user(user :: Lightning.Accounts.User.t()) :: [
  Lightning.Projects.Project.t()
]

Fetches projects for a given user from the database.

Parameters

  • user: The user struct for which projects are being queried.
  • opts: Keyword list of options including :include for associations to preload and :order_by for sorting.

Returns

  • A list of projects associated with the user.

get_projects_overview(user, opts \\ [])

get_workspace_projects(parent)

@spec get_workspace_projects(Lightning.Projects.Project.t()) :: [
  Lightning.Projects.Project.t()
]

Returns a read-only “workspace” view for a parent project: the parent itself plus all of its direct sandboxes (unique set, no preloads).

Use this for nav/filters where showing the parent alongside its sandboxes is needed.

Notes

  • Order is not guaranteed. Sort the resulting list if a specific order is needed.
  • Not recursive. If we later model deeper hierarchies, use a recursive CTE.

invite_collaborators(project, collaborators, inviter)

list_project_admin_emails(id)

@spec list_project_admin_emails(Ecto.UUID.t()) :: [String.t(), ...] | []

Lists emails of users with :owner or :admin roles in the project

list_project_credentials(project)

@spec list_project_credentials(project :: Lightning.Projects.Project.t()) :: [
  Lightning.Projects.ProjectCredential.t()
]

list_project_files(project, opts \\ [])

list_projects()

Returns the list of projects.

Examples

iex> list_projects()
[%Project{}, ...]

list_projects_having_history_retention()

@spec list_projects_having_history_retention() ::
  [] | [Lightning.Projects.Project.t(), ...]

Lists all projects that have history retention

list_sandboxes(parent_id)

@spec list_sandboxes(Ecto.UUID.t()) :: [Lightning.Projects.Project.t()]

Returns the direct sandboxes (children) of a parent project, ordered by name (ASC).

This is a flat view: only rows where parent.id == child.parent_id are returned. If we later support arbitrarily deep nesting, switch this to a recursive CTE.

list_workflows(project)

member_of?(project, user)

perform(job)

Perform, when called with %{"type" => "purge_deleted"} will find projects that are ready for permanent deletion and purge them.

project_credentials_query(project)

project_dataclips_query(project)

project_jobs_query(project)

project_run_step_query(project)

project_runs_query(project)

project_steps_query(project)

project_triggers_query(project)

project_users_query(project)

@spec project_users_query(atom() | %{:id => any(), optional(any()) => any()}) ::
  Ecto.Query.t()

project_workflows_query(project)

project_workorders_query(project)

projects_for_user_query(user)

@spec projects_for_user_query(user :: Lightning.Accounts.User.t()) ::
  Ecto.Queryable.t()

Builds a query to retrieve projects associated with a user.

Parameters

  • user: The user struct for which projects are being queried.
  • opts: Keyword list of options including :include for associations to preload and :order_by for sorting.

Returns

  • An Ecto queryable struct to fetch projects.

provision_sandbox(parent, actor, attrs)

@spec provision_sandbox(
  Lightning.Projects.Project.t(),
  Lightning.Accounts.User.t(),
  map()
) ::
  {:ok, Lightning.Projects.Project.t()} | {:error, term()}

Provisions a sandbox (child) project under the given parent.

This is a thin wrapper around Lightning.Projects.Sandboxes.provision/3.

Authorization

The actor must be an :owner or :admin of the parent project. Returns {:error, :unauthorized} otherwise.

Attributes (attrs)

  • :name (required) — Sandbox name (must be unique per parent_id).
  • :color — Optional hex color (string) for the project.
  • :env — Optional environment slug to set on the project.
  • :collaborators — Optional list of %{user_id: Ecto.UUID.t(), role: atom()}. The actor is always added as :owner; extra :owner entries are ignored.
  • :dataclip_ids — Optional list of named dataclip IDs to copy (eligible types: :global | :saved_input | :http_request).

Behavior

  • Clones a subset of project settings from the parent.
  • References existing credentials via project_credentials (no credential duplication).
  • Clones the workflow DAG (jobs, triggers, edges), disables triggers in the clone, remaps node positions, and copies the latest workflow head/version per workflow.
  • Does not copy runs/history.

Examples

iex> provision_sandbox(parent, actor, %{name: "SB-1", color: "#aabbcc"})
{:ok, %Lightning.Projects.Project{}}

iex> provision_sandbox(parent, viewer, %{name: "SB-1"})
{:error, :unauthorized}

root_of(p)

Returns the root ancestor of a project by walking up parent_id links.

Supports arbitrarily deep nesting. (Assumes the parent chain is well-formed.)

save_dataclips?(id)

Should input or output dataclips be saved for runs in this project?

schedule_project_deletion(project)

Given a project, this function sets a scheduled deletion date based on the PURGE_DELETED_AFTER_DAYS environment variable. If no ENV is set, this date defaults to NOW but the automatic project purge cronjob will never run. (Note that subsequent logins will be blocked for projects pending deletion.)

scheduled_project_deletion_changes(multi, list)

select_first_project_for_user(user)

@spec select_first_project_for_user(user :: Lightning.Accounts.User.t()) ::
  Lightning.Projects.Project.t() | nil

subscribe()

See Lightning.Projects.Events.subscribe/0.

update_project(project, attrs, user \\ nil)

Updates a project.

Examples

iex> update_project(project, %{field: new_value})
{:ok, %Project{}}

iex> update_project(project, %{field: bad_value})
{:error, %Ecto.Changeset{}}

update_project_user(project_user, attrs)

Updates a project user.

Examples

iex> update_project_user(project_user, %{field: new_value})
{:ok, %ProjectUser{}}

iex> update_project_user(projectUser, %{field: bad_value})
{:error, %Ecto.Changeset{}}

update_project_with_users(project, attrs, notify_users \\ true)

@spec update_project_with_users(Lightning.Projects.Project.t(), map(), boolean()) ::
  {:ok, Lightning.Projects.Project.t()} | {:error, Ecto.Changeset.t()}

validate_for_deletion(project, attrs)

Returns an %Ecto.Changeset{} for changing the project scheduled_deletion.

Examples

iex> validate_for_deletion(project)
%Ecto.Changeset{data: %Project{}}