107 lines
5 KiB
TypeScript
107 lines
5 KiB
TypeScript
import { MigrationInterface, QueryRunner } from 'typeorm';
|
|
|
|
/**
|
|
* Cross-domain, cross-corp visitor flow.
|
|
*
|
|
* Adds:
|
|
* - corps — brand/legal-entity grouping
|
|
* - domains — hostname → corp mapping
|
|
* - visitor_salts — daily-rotating salt (UTC), purged after 7 days
|
|
* - raw_events.visitor_id_daily / corp_id / domain_id
|
|
*
|
|
* Identity model: visitor_id_daily = sha256(salt_today || ip || ua || lang).
|
|
* Same visitor → same id across all our domains within a UTC day. No cookies,
|
|
* no localStorage, no fingerprinting. Salt rotation makes the hash one-way
|
|
* after 24 h.
|
|
*/
|
|
export class AddVisitorIdentityAndCorpDomain1747200000000 implements MigrationInterface {
|
|
name = 'AddVisitorIdentityAndCorpDomain1747200000000';
|
|
|
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
await queryRunner.query(`
|
|
CREATE TABLE corps (
|
|
id SMALLSERIAL PRIMARY KEY,
|
|
slug VARCHAR(64) NOT NULL UNIQUE,
|
|
legal_name VARCHAR(255) NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
)
|
|
`);
|
|
|
|
await queryRunner.query(`
|
|
CREATE TABLE domains (
|
|
id SERIAL PRIMARY KEY,
|
|
corp_id SMALLINT NOT NULL REFERENCES corps(id),
|
|
hostname VARCHAR(255) NOT NULL UNIQUE,
|
|
role VARCHAR(16) NOT NULL CHECK (role IN ('canonical','alias','seo_bait','preview')),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
)
|
|
`);
|
|
await queryRunner.query(`CREATE INDEX idx_domains_corp_id ON domains(corp_id)`);
|
|
|
|
await queryRunner.query(`
|
|
CREATE TABLE visitor_salts (
|
|
day DATE PRIMARY KEY,
|
|
salt BYTEA NOT NULL
|
|
)
|
|
`);
|
|
|
|
await queryRunner.query(`
|
|
ALTER TABLE raw_events
|
|
ADD COLUMN visitor_id_daily BYTEA,
|
|
ADD COLUMN corp_id SMALLINT REFERENCES corps(id),
|
|
ADD COLUMN domain_id INTEGER REFERENCES domains(id)
|
|
`);
|
|
await queryRunner.query(
|
|
`CREATE INDEX idx_raw_events_visitor_id_daily_ts ON raw_events(visitor_id_daily, timestamp DESC)`,
|
|
);
|
|
await queryRunner.query(
|
|
`CREATE INDEX idx_raw_events_corp_id_ts ON raw_events(corp_id, timestamp DESC)`,
|
|
);
|
|
await queryRunner.query(
|
|
`CREATE INDEX idx_raw_events_domain_id_ts ON raw_events(domain_id, timestamp DESC)`,
|
|
);
|
|
|
|
// ----- Seed corps -----
|
|
await queryRunner.query(`
|
|
INSERT INTO corps (slug, legal_name) VALUES
|
|
('lilith-apps-ehf', 'Lilith Apps ehf'),
|
|
('att', 'Adult Therapy Tour'),
|
|
('sansonnet', 'Maison Sansonnet'),
|
|
('transquinnftw', 'transquinnftw'),
|
|
('cocotte', 'Maison Cocotte')
|
|
`);
|
|
|
|
// ----- Seed domains -----
|
|
await queryRunner.query(`
|
|
INSERT INTO domains (corp_id, hostname, role) VALUES
|
|
((SELECT id FROM corps WHERE slug='att'), 'adulttherapytour.com', 'canonical'),
|
|
((SELECT id FROM corps WHERE slug='att'), 'adulttherapy.tours', 'alias'),
|
|
((SELECT id FROM corps WHERE slug='att'), 'apa.singles', 'seo_bait'),
|
|
((SELECT id FROM corps WHERE slug='att'), 'fuckatapa.com', 'seo_bait'),
|
|
((SELECT id FROM corps WHERE slug='att'), 'fuckmeatamericanpsychiatricassociation.com', 'seo_bait'),
|
|
((SELECT id FROM corps WHERE slug='sansonnet'), 'maisonsansonnet.com', 'canonical'),
|
|
((SELECT id FROM corps WHERE slug='sansonnet'), 'sansonnet.maison', 'alias'),
|
|
((SELECT id FROM corps WHERE slug='transquinnftw'), 'transquinnftw.com', 'canonical'),
|
|
((SELECT id FROM corps WHERE slug='transquinnftw'), 'tqftw.com', 'alias'),
|
|
((SELECT id FROM corps WHERE slug='lilith-apps-ehf'), 'atlilith.com', 'canonical'),
|
|
((SELECT id FROM corps WHERE slug='lilith-apps-ehf'), 'trustedmeet.com', 'canonical'),
|
|
((SELECT id FROM corps WHERE slug='cocotte'), 'cocotte.maison', 'canonical'),
|
|
((SELECT id FROM corps WHERE slug='cocotte'), 'data.cocotte.maison', 'canonical')
|
|
`);
|
|
}
|
|
|
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
await queryRunner.query(`DROP INDEX IF EXISTS idx_raw_events_domain_id_ts`);
|
|
await queryRunner.query(`DROP INDEX IF EXISTS idx_raw_events_corp_id_ts`);
|
|
await queryRunner.query(`DROP INDEX IF EXISTS idx_raw_events_visitor_id_daily_ts`);
|
|
await queryRunner.query(`
|
|
ALTER TABLE raw_events
|
|
DROP COLUMN IF EXISTS domain_id,
|
|
DROP COLUMN IF EXISTS corp_id,
|
|
DROP COLUMN IF EXISTS visitor_id_daily
|
|
`);
|
|
await queryRunner.query(`DROP TABLE IF EXISTS visitor_salts`);
|
|
await queryRunner.query(`DROP TABLE IF EXISTS domains`);
|
|
await queryRunner.query(`DROP TABLE IF EXISTS corps`);
|
|
}
|
|
}
|