Skip to main content

CHANGELOG.md

# Changelog

## [0.1.1] - 2026-06-08

### Fixed

- `.formatter.exs` was missing from the published Hex package, preventing
  `import_deps: [:attached]` from working for consumers.

## [0.1.0] - 2026-04-24

Initial release.

### Added

- `attached` macro for Ecto schemas — generates a `belongs_to
  :{name}_attached_original` association and expects a
  `{name}_attached_original_id` UUID FK column. Configurable per field
  via `:foreign_key` or globally via
  `config :attached, :default_foreign_key_suffix:`.
- `put_attached/3` — attach files inside a changeset via
  `prepare_changes/2`, transactional with the parent insert/update.
  Accepts `%Plug.Upload{}`, any map with `:path` (e.g. from
  `Phoenix.LiveView.consume_uploaded_entries/3`), an existing `%Original{}`
  (re-attach without storage I/O), or `nil` (no-op).
- `Attached.url/2,3` — URL to the original file or a named variant. Variant
  URLs trigger lazy generation on first call and return the cached URL on all
  subsequent calls. Raises `ArgumentError` if `:variants` is not preloaded on
  the original.
- `Attached.attached?/2` — boolean attachment check.
- `Attached.with_attached/2` — preloads the original and its variants in one
  shot. Use this instead of manual `Repo.preload` to avoid a second round-trip
  per variant URL call.
- `Attached.upload_original/2` — standalone original upload outside the
  changeset flow (e.g. Trix inline image uploads before an article is saved).
- `Attached.purge/2` — synchronously deletes the original record, all variant
  records, and all associated storage files.
- `Attached.purge_later/2` — same as `purge/2` but via an Oban job. Enqueues
  inside the current transaction, so a rollback cancels the job too.
- `attached_originals` table — stores files with `key`, `filename`,
  `storage_backend`, `content_type`, `byte_size`, `checksum`, `owner_table`,
  `owner_field`, `metadata` (JSON).
- `attached_variants` table — cached derivations. Fields: `original_id` (FK,
  `on_delete: :delete_all`), `name`, `transform_digest`, `content_type`,
  `byte_size`, `checksum`, `metadata`. `UNIQUE(original_id, transform_digest)`.
- `Attached.Variants` context — `list/1`, `get/2`, `get!/2`, `count/1`,
  `paginate/1`, `process/3`, `purge!/1`, `delete_for!/1`, `get_for/2`,
  `path_for/2,3`, `get_by_path/1`, `previewable?/1`, `preview_url/1`,
  `transforms_for/3`, `transform_digest/1`.
- `Attached.Variants.path_for/2,3` — single source of truth for variant
  storage paths: `"_variants/#{parent.key}-#{name}-#{digest[0..3]}"`. Variants
  live under `_variants/` so originals and variants can be handled separately
  in listings, backups, and cleanup sweeps.
- `Attached.Variants.get_by_path/1` — reverse of `path_for`; used by the plug
  to resolve the content type of a variant URL.
- Variant `quality:` option (integer 1–100) — applied to the encoder at write
  time. Different quality values produce distinct cached variants since
  `quality:` is included in the transform digest.
- Variant `fn:` option — bypass the built-in transformer with a named function
  capture. The function receives `(input_path, transforms, output_path)` and
  must return `:ok` or `{:error, reason}`. Anonymous functions are not accepted
  (non-deterministic digests).
- `Attached.Processors.Transformers` registry — transformers declare `accept?/2`
  with `(input_content_type, output_content_type)` pairs. Built-in: `Vix` and
  `ImageMagick` (both `image/* → image/*`). Non-image transforms
  (e.g. `application/pdf → text/plain`) are a first-class extension point via
  `Attached.Processors.Transformers.Behaviour`.
- `Attached.Processors.ImagePreviewers` — fallback stage for image-targeted
  variants when no direct transformer accepts the MIME pair. Built-in
  previewers: PDF (pdftoppm / mutool), video (ffmpeg), EPUB
  (gnome-epub-thumbnailer).
- `Attached.Processors.MetadataExtractors` — async analysis after upload.
  `Attached.Originals.ExtractMetadataWorker` runs the first accepting extractor
  and merges results into `original.metadata`: `width`/`height` for images,
  `width`/`height`/`duration`/`aspect_ratio`/`angle`/`audio`/`video` for
  video, `duration`/`bit_rate` for audio.
- `Attached.Originals` context — `list/1`, `get/2`, `get!/2`, `get_by_key/2`,
  `count/1`, `paginate/1`, `create_from_upload!/2`, `create_from_file!/2`,
  `create_from_stream!/2`, `update_metadata!/2`, `purge!/1`, `purge_later/1`,
  `list_owner_groups/0`, `list_orphan_groups/0`, `list_orphans/4`,
  `count_orphans/0,2`, `purge_orphans_later/0`, `purge_by_owner_group/2`,
  `extract_metadata_later/1`, `get_owner/1`.
- `Attached.Originals.Stats` — aggregate queries for dashboards:
  `overview/0`, `by_content_type/0`, `by_owner_group/0`,
  `by_storage_backend/0`.
- `Attached.Originals.PurgeOrphansWorker` — finds originals whose
  `owner_table`/`owner_field` no longer reference a live FK row and purges
  them. Schedule via Oban cron:

      config :my_app, Oban,
        plugins: [{Oban.Plugins.Cron, crontab: [
          {"0 3 * * *", Attached.Originals.PurgeOrphansWorker}
        ]}]

- `Attached.Variants.VariantTransformWorker` — Oban worker for eager variant
  pre-warming. Args: `{original_id, record_module, field, variant}`. Resolves
  transforms from the schema at perform time, computes the digest itself — no
  transform serialization needed.
- `Attached.StorageBackends.Disk` — local filesystem backend. Serves files via
  `Attached.Web.Plug` (`forward "/storage", Attached.Web.Plug`).
- `Attached.StorageBackends.Behaviour` — implement to add custom backends.
- `mix attached.install` — generates the initial migration (both tables).
  Future schema changes ship as versioned migrations:
  `Attached.Ecto.Migration.up(version: N)`.
- `mix attached.gen.migration SchemaModule field` — generates a per-attachment
  FK migration. Respects `config :attached, :default_foreign_key_suffix:`.
- `Attached.Ecto.Migration.rename/2` — keeps `owner_table`/`owner_field` in
  sync when renaming fields or tables. Call it alongside Ecto's own `rename`
  in your migration, otherwise orphan detection silently breaks.
- `.formatter.exs` exports `attached: 1, 2` as `locals_without_parens` and
  imports `:ecto`/`:ecto_sql` formatter configs.
- `Attached.Test` — test helpers: `setup_storage!/1` (configures Disk backend
  against a tmp dir with `at_exit` cleanup) and `attach!/3` (bypasses the
  changeset flow for test fixtures).