feat(db): retention schema for account deletion (migration 00007)
Additive/expand-contract: new append-only retained_identities table (the legal dossier of every detached credential, keyed by account_id, TTL'd by detached_at) plus nullable accounts columns last_login_at/last_login_ip (cold-load stamp) and deleted_at/deleted_display_name (tombstone + retained real name). Regenerates the go-jet models for accounts + retained_identities only.
This commit is contained in:
@@ -32,4 +32,8 @@ type Accounts struct {
|
||||
MergedAt *time.Time
|
||||
FlaggedHighRateAt *time.Time
|
||||
VariantPreferences pq.StringArray
|
||||
LastLoginAt *time.Time
|
||||
LastLoginIP *string
|
||||
DeletedAt *time.Time
|
||||
DeletedDisplayName *string
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"time"
|
||||
)
|
||||
|
||||
type RetainedIdentities struct {
|
||||
RetainedID uuid.UUID `sql:"primary_key"`
|
||||
AccountID uuid.UUID
|
||||
Kind string
|
||||
ExternalID string
|
||||
Confirmed bool
|
||||
LinkedAt time.Time
|
||||
DetachedAt time.Time
|
||||
Reason string
|
||||
}
|
||||
@@ -35,6 +35,10 @@ type accountsTable struct {
|
||||
MergedAt postgres.ColumnTimestampz
|
||||
FlaggedHighRateAt postgres.ColumnTimestampz
|
||||
VariantPreferences postgres.ColumnStringArray
|
||||
LastLoginAt postgres.ColumnTimestampz
|
||||
LastLoginIP postgres.ColumnString
|
||||
DeletedAt postgres.ColumnTimestampz
|
||||
DeletedDisplayName postgres.ColumnString
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
@@ -94,8 +98,12 @@ func newAccountsTableImpl(schemaName, tableName, alias string) accountsTable {
|
||||
MergedAtColumn = postgres.TimestampzColumn("merged_at")
|
||||
FlaggedHighRateAtColumn = postgres.TimestampzColumn("flagged_high_rate_at")
|
||||
VariantPreferencesColumn = postgres.StringArrayColumn("variant_preferences")
|
||||
allColumns = postgres.ColumnList{AccountIDColumn, DisplayNameColumn, PreferredLanguageColumn, TimeZoneColumn, BlockChatColumn, BlockFriendRequestsColumn, CreatedAtColumn, UpdatedAtColumn, AwayStartColumn, AwayEndColumn, HintBalanceColumn, IsGuestColumn, NotificationsInAppOnlyColumn, PaidAccountColumn, MergedIntoColumn, MergedAtColumn, FlaggedHighRateAtColumn, VariantPreferencesColumn}
|
||||
mutableColumns = postgres.ColumnList{DisplayNameColumn, PreferredLanguageColumn, TimeZoneColumn, BlockChatColumn, BlockFriendRequestsColumn, CreatedAtColumn, UpdatedAtColumn, AwayStartColumn, AwayEndColumn, HintBalanceColumn, IsGuestColumn, NotificationsInAppOnlyColumn, PaidAccountColumn, MergedIntoColumn, MergedAtColumn, FlaggedHighRateAtColumn, VariantPreferencesColumn}
|
||||
LastLoginAtColumn = postgres.TimestampzColumn("last_login_at")
|
||||
LastLoginIPColumn = postgres.StringColumn("last_login_ip")
|
||||
DeletedAtColumn = postgres.TimestampzColumn("deleted_at")
|
||||
DeletedDisplayNameColumn = postgres.StringColumn("deleted_display_name")
|
||||
allColumns = postgres.ColumnList{AccountIDColumn, DisplayNameColumn, PreferredLanguageColumn, TimeZoneColumn, BlockChatColumn, BlockFriendRequestsColumn, CreatedAtColumn, UpdatedAtColumn, AwayStartColumn, AwayEndColumn, HintBalanceColumn, IsGuestColumn, NotificationsInAppOnlyColumn, PaidAccountColumn, MergedIntoColumn, MergedAtColumn, FlaggedHighRateAtColumn, VariantPreferencesColumn, LastLoginAtColumn, LastLoginIPColumn, DeletedAtColumn, DeletedDisplayNameColumn}
|
||||
mutableColumns = postgres.ColumnList{DisplayNameColumn, PreferredLanguageColumn, TimeZoneColumn, BlockChatColumn, BlockFriendRequestsColumn, CreatedAtColumn, UpdatedAtColumn, AwayStartColumn, AwayEndColumn, HintBalanceColumn, IsGuestColumn, NotificationsInAppOnlyColumn, PaidAccountColumn, MergedIntoColumn, MergedAtColumn, FlaggedHighRateAtColumn, VariantPreferencesColumn, LastLoginAtColumn, LastLoginIPColumn, DeletedAtColumn, DeletedDisplayNameColumn}
|
||||
defaultColumns = postgres.ColumnList{DisplayNameColumn, PreferredLanguageColumn, TimeZoneColumn, BlockChatColumn, BlockFriendRequestsColumn, CreatedAtColumn, UpdatedAtColumn, AwayStartColumn, AwayEndColumn, HintBalanceColumn, IsGuestColumn, NotificationsInAppOnlyColumn, PaidAccountColumn, VariantPreferencesColumn}
|
||||
)
|
||||
|
||||
@@ -121,6 +129,10 @@ func newAccountsTableImpl(schemaName, tableName, alias string) accountsTable {
|
||||
MergedAt: MergedAtColumn,
|
||||
FlaggedHighRateAt: FlaggedHighRateAtColumn,
|
||||
VariantPreferences: VariantPreferencesColumn,
|
||||
LastLoginAt: LastLoginAtColumn,
|
||||
LastLoginIP: LastLoginIPColumn,
|
||||
DeletedAt: DeletedAtColumn,
|
||||
DeletedDisplayName: DeletedDisplayNameColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package table
|
||||
|
||||
import (
|
||||
"github.com/go-jet/jet/v2/postgres"
|
||||
)
|
||||
|
||||
var RetainedIdentities = newRetainedIdentitiesTable("backend", "retained_identities", "")
|
||||
|
||||
type retainedIdentitiesTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
RetainedID postgres.ColumnString
|
||||
AccountID postgres.ColumnString
|
||||
Kind postgres.ColumnString
|
||||
ExternalID postgres.ColumnString
|
||||
Confirmed postgres.ColumnBool
|
||||
LinkedAt postgres.ColumnTimestampz
|
||||
DetachedAt postgres.ColumnTimestampz
|
||||
Reason postgres.ColumnString
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
DefaultColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type RetainedIdentitiesTable struct {
|
||||
retainedIdentitiesTable
|
||||
|
||||
EXCLUDED retainedIdentitiesTable
|
||||
}
|
||||
|
||||
// AS creates new RetainedIdentitiesTable with assigned alias
|
||||
func (a RetainedIdentitiesTable) AS(alias string) *RetainedIdentitiesTable {
|
||||
return newRetainedIdentitiesTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new RetainedIdentitiesTable with assigned schema name
|
||||
func (a RetainedIdentitiesTable) FromSchema(schemaName string) *RetainedIdentitiesTable {
|
||||
return newRetainedIdentitiesTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new RetainedIdentitiesTable with assigned table prefix
|
||||
func (a RetainedIdentitiesTable) WithPrefix(prefix string) *RetainedIdentitiesTable {
|
||||
return newRetainedIdentitiesTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new RetainedIdentitiesTable with assigned table suffix
|
||||
func (a RetainedIdentitiesTable) WithSuffix(suffix string) *RetainedIdentitiesTable {
|
||||
return newRetainedIdentitiesTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newRetainedIdentitiesTable(schemaName, tableName, alias string) *RetainedIdentitiesTable {
|
||||
return &RetainedIdentitiesTable{
|
||||
retainedIdentitiesTable: newRetainedIdentitiesTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newRetainedIdentitiesTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newRetainedIdentitiesTableImpl(schemaName, tableName, alias string) retainedIdentitiesTable {
|
||||
var (
|
||||
RetainedIDColumn = postgres.StringColumn("retained_id")
|
||||
AccountIDColumn = postgres.StringColumn("account_id")
|
||||
KindColumn = postgres.StringColumn("kind")
|
||||
ExternalIDColumn = postgres.StringColumn("external_id")
|
||||
ConfirmedColumn = postgres.BoolColumn("confirmed")
|
||||
LinkedAtColumn = postgres.TimestampzColumn("linked_at")
|
||||
DetachedAtColumn = postgres.TimestampzColumn("detached_at")
|
||||
ReasonColumn = postgres.StringColumn("reason")
|
||||
allColumns = postgres.ColumnList{RetainedIDColumn, AccountIDColumn, KindColumn, ExternalIDColumn, ConfirmedColumn, LinkedAtColumn, DetachedAtColumn, ReasonColumn}
|
||||
mutableColumns = postgres.ColumnList{AccountIDColumn, KindColumn, ExternalIDColumn, ConfirmedColumn, LinkedAtColumn, DetachedAtColumn, ReasonColumn}
|
||||
defaultColumns = postgres.ColumnList{ConfirmedColumn, DetachedAtColumn}
|
||||
)
|
||||
|
||||
return retainedIdentitiesTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
RetainedID: RetainedIDColumn,
|
||||
AccountID: AccountIDColumn,
|
||||
Kind: KindColumn,
|
||||
ExternalID: ExternalIDColumn,
|
||||
Confirmed: ConfirmedColumn,
|
||||
LinkedAt: LinkedAtColumn,
|
||||
DetachedAt: DetachedAtColumn,
|
||||
Reason: ReasonColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
DefaultColumns: defaultColumns,
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@ func UseSchema(schema string) {
|
||||
GameSetupDraws = GameSetupDraws.FromSchema(schema)
|
||||
Games = Games.FromSchema(schema)
|
||||
Identities = Identities.FromSchema(schema)
|
||||
RetainedIdentities = RetainedIdentities.FromSchema(schema)
|
||||
Sessions = Sessions.FromSchema(schema)
|
||||
SuspensionReasons = SuspensionReasons.FromSchema(schema)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
-- Account deletion as legal retention (not erasure). Two additive pieces.
|
||||
--
|
||||
-- retained_identities is an append-only journal of every credential detached from an
|
||||
-- account — on unlink, email change, or account deletion (reason). It preserves the legal
|
||||
-- dossier (which email/vk/tg was linked, and when) while the live identities row is
|
||||
-- removed, so the (kind, external_id) frees for a new account to reuse. No unique
|
||||
-- constraint and no kind CHECK: the same credential may recur across accounts and events,
|
||||
-- and the log stays robust to future identity kinds. detached_at drives the retention TTL.
|
||||
--
|
||||
-- accounts gains: last_login_at / last_login_ip (stamped on a cold app-load, throttled),
|
||||
-- deleted_at (the tombstone marker), and deleted_display_name (the real name retained for
|
||||
-- the admin dossier after the live display_name is scrubbed to the anonymised label).
|
||||
--
|
||||
-- Expand-contract: everything is additive (a new table plus nullable columns), so a
|
||||
-- backend image rollback stays DB-safe — older code simply ignores them. The accounts
|
||||
-- table shape changes, so its generated go-jet model is regenerated.
|
||||
|
||||
-- +goose Up
|
||||
CREATE TABLE backend.retained_identities (
|
||||
retained_id uuid NOT NULL,
|
||||
account_id uuid NOT NULL,
|
||||
kind text NOT NULL,
|
||||
external_id text NOT NULL,
|
||||
confirmed boolean DEFAULT false NOT NULL,
|
||||
linked_at timestamp with time zone NOT NULL,
|
||||
detached_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
reason text NOT NULL,
|
||||
CONSTRAINT retained_identities_pkey PRIMARY KEY (retained_id),
|
||||
CONSTRAINT retained_identities_reason_chk
|
||||
CHECK ((reason = ANY (ARRAY['unlink'::text, 'change'::text, 'delete'::text])))
|
||||
);
|
||||
CREATE INDEX retained_identities_account_id_idx ON backend.retained_identities (account_id);
|
||||
CREATE INDEX retained_identities_detached_at_idx ON backend.retained_identities (detached_at);
|
||||
|
||||
ALTER TABLE backend.accounts
|
||||
ADD COLUMN last_login_at timestamp with time zone,
|
||||
ADD COLUMN last_login_ip text,
|
||||
ADD COLUMN deleted_at timestamp with time zone,
|
||||
ADD COLUMN deleted_display_name text;
|
||||
|
||||
-- +goose Down
|
||||
ALTER TABLE backend.accounts
|
||||
DROP COLUMN last_login_at,
|
||||
DROP COLUMN last_login_ip,
|
||||
DROP COLUMN deleted_at,
|
||||
DROP COLUMN deleted_display_name;
|
||||
DROP TABLE backend.retained_identities;
|
||||
Reference in New Issue
Block a user