asobi_player_export (asobi v0.75.1)

View Source

Everything core holds about one player, as one map.

The read half of asobi_player_erase, and it covers the same tables in the same order, so "what would erasure delete" and "what would export return" stay one list rather than two that drift.

Positive allowlists, never maps:without/2

Every row goes through a maps:with/2 projection, modelled on asobi_ops_players:project/1. players carries hashed_password, so a subtractive filter is one schema field away from exporting a credential; player_tokens carries live bearer tokens, so the export reports that a session existed and when, never the token itself.

The two tables with no foreign key

match_records.players is a jsonb list of ids and votes.votes_cast is a jsonb object keyed by voter id. Neither has a foreign key, and both are records about a player whatever the schema says about referential integrity, so both are included - by jsonb containment and jsonb key existence respectively.

votes is projected differently from every other table here: the row holds everybody's votes, so exporting votes_cast whole would hand one player the private choices of everyone else in the match. Only this player's own choice is lifted out. A privacy feature that leaks is worse than one that is missing.

zone_snapshots.entities is deliberately absent. It is opaque game-defined state whose shape core does not know - a game may key entities by player id or bury one inside entity state, and there is no projection core could apply to it that would not either miss the data or export another player's. A game that puts personal data there owns exporting it, the same way it owns erasing it.

No extension callback

There is deliberately no export_player/1 for extensions in this change. asobi_extension:erase_player/1 earns its keep because the foreign key physically forces an author to answer it; an export callback has no forcing function, so an extension that skipped it would produce a silently incomplete export and nothing would fail. That is a worse contract than no contract. If one ever ships, the payload must name which installed extensions contributed and which did not.

Summary

Functions

Every core row that names PlayerId, projected.

Functions

run(PlayerId)

-spec run(binary()) -> {ok, map()} | {error, not_found}.

Every core row that names PlayerId, projected.

{error, not_found} when no such player exists - an empty export and a missing account are different answers to a data-subject request.