MishkaBlog.Dev.Seeds (Mishka Blog v0.0.1)

Copy Markdown View Source

Dev and stress seeds for MishkaBlog resources.

"Archived" is seeded exactly one way: Ash.destroy!/2 through a resource's :destroy action, in the very last pass of a run. That writes the archived_at soft-delete timestamp AND force-sets status: :archived on BlogPost and Collection, the two resources whose status enum carries that value.

Nothing else ever writes status: :archived. The status pool in MishkaBlog.Dev.Generator leaves it out, and the seeds no longer mark live rows with the label: only archived_at decides which tab a row appears in, so a labelled-but-live post reads to an admin as an archived post sitting in All Posts. Seeded data therefore satisfies status :archived if and only if archived_at is set, which MishkaBlog.Dev.SeedsArchivalTest pins.

Comment, Tag, Blogger, Team and AutoPosting have no :archived status at all: for them the soft delete is the only archival concept, and their destroy actions leave the status untouched — an archived :spam comment keeps its :spam label.

Receives foundation data from MishkaCmsCore.Dev.Seeds.dev_seed/0.

Summary

Functions

Executes the plan, children before parents, and reads one row back through :get_archived to prove the rows really moved to the Archived tab.

Chooses the rows that will be soft-deleted, without touching the database.

Spreads creation timestamps over the last 18 months.

Creates realistic blog seed data for a single site: multilingual posts, threaded comments, views, engagement rows and, as the final pass, a soft-deleted minority that populates the admin's Archived tab.

Fills the engagement tables the admin aggregates over — ratings, bookmarks, comment votes, blogger and team followers — so the star, bookmark and follower columns are not hard-zero.

Keeps only the users that may legitimately author content on this site: master users (site_id nil) and tenant users of this very site.

Stress seed: the same building blocks as dev_seed/3 — multilingual posts, views, engagement and the archival pass — at large volumes, so stress data exercises the code paths the admin renders. The :posts option is the total post count across all five languages.

Functions

archive_pass(site_id, plan)

Executes the plan, children before parents, and reads one row back through :get_archived to prove the rows really moved to the Archived tab.

Ash.destroy!/2 on these resources is a soft delete: AshArchival rewrites the :destroy action to set archived_at instead of removing the row. :permanent_destroy is the one that really deletes and is never used here. On BlogPost and Collection the destroy also force-sets status: :archived; on Comment, Tag, Blogger and AutoPosting the status is left alone, so an archived :spam comment keeps its moderation label.

A team's MishkaBlog.TeamMember rows are left in place: that join resource has no archival, so destroying them would be a real delete and the membership could not come back with an unarchive.

Nothing may write to a record after this pass — the prevent_archived_*_updates triggers raise on any update of an archived row.

archive_plan(site_id, data)

Chooses the rows that will be soft-deleted, without touching the database.

The slice is picked from a hash of {site_id, record_id, :archive} so the archived set is the same on every run, and it stays a genuine minority: a soft delete is a full Ash destroy that writes a PaperTrail version, emits an event and broadcasts over PubSub.

Two rules shape the selection. Language masters are only archived together with their whole group, since a master with live translations is a state the app declares invalid. And post comments are archived alongside their post, because no resource declares archive_related — an orphan comment would render as "On: Unknown".

backdate(site_id, map)

Spreads creation timestamps over the last 18 months.

inserted_at is a create_timestamp and appears in no accept list, so Ash.Seed.update!/3 is the only Ash-native way to set it. That call goes straight to the data layer: no action, no validation, no notifier and no PaperTrail version, which is why a backdated row's inserted_at will not match the timestamp of its own first version. It also applies update defaults, so updated_at has to be passed explicitly or it silently resets to now.

This pass must run before archive_pass/2: the prevent_archived_*_updates triggers raise on any update of a row that already has an archived_at.

dev_seed(site_id, users, progress \\ fn _ -> :ok end)

@spec dev_seed(String.t(), [struct()], (String.t() -> any())) ::
  map() | :already_seeded

Creates realistic blog seed data for a single site: multilingual posts, threaded comments, views, engagement rows and, as the final pass, a soft-deleted minority that populates the admin's Archived tab.

Per-site volumes are derived from a hash of site_id, so a run is reproducible while the three sites differ from one another. users should include both master and tenant users; users belonging to another site are filtered out before bloggers and comment authors are picked.

RUN AGAIN ON A SITE THAT HAS A BLOG, this adds no generated row and answers :already_seeded. Every row it generates has no name to recognise it by afterwards, so what is either there or not is the blog itself, and a site that has one keeps exactly the posts, comments and ratings it has rather than being given a second set beside them. The locked and unlisted examples are recognised by their slugs, so a re-run writes only the ones the site is missing.

seed_engagement(map, users, plan)

Fills the engagement tables the admin aggregates over — ratings, bookmarks, comment votes, blogger and team followers — so the star, bookmark and follower columns are not hard-zero.

MishkaBlog.TeamPermission is not seeded: dev_seed/3 receives only sites and users, and a team permission needs a MishkaCmsCore.Accounts.Permission record, which the core seed does not hand to the extension seeds.

Archived posts and comments are skipped, so the aggregates the admin renders describe live rows.

site_users(users, site_id)

Keeps only the users that may legitimately author content on this site: master users (site_id nil) and tenant users of this very site.

MishkaCmsCore.Dev.Seeds hands every site the same "master" list, and that list contains none@example.com, which is created as a tenant user of the localhost site. Without this filter a blogger or comment author on one site can be another site's user.

stress_seed(site_id, users, opts \\ [])

Stress seed: the same building blocks as dev_seed/3 — multilingual posts, views, engagement and the archival pass — at large volumes, so stress data exercises the code paths the admin renders. The :posts option is the total post count across all five languages.