feat(api): ✨ Add migration and seed script for visitor_identity and corp_domain tables
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
8826e7129a
commit
16da6276d2
2 changed files with 132 additions and 0 deletions
28
infrastructure/seed-cross-domain.sql
Normal file
28
infrastructure/seed-cross-domain.sql
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
-- Cross-domain, cross-corp visitor flow — manual bootstrap.
|
||||
-- Mirrors services/api/migrations/1747200000000-AddVisitorIdentityAndCorpDomain.ts
|
||||
-- for environments where no TypeORM migration runner is configured.
|
||||
--
|
||||
-- Apply against the analytics Postgres DB after the collector boots once
|
||||
-- (so TypeORM synchronize creates corps/domains/visitor_salts table shells
|
||||
-- from the entity definitions). This script only inserts the seed rows.
|
||||
|
||||
INSERT INTO corps (slug, legal_name) VALUES
|
||||
('lilith-apps-ehf', 'Lilith Apps ehf'),
|
||||
('att', 'Adult Therapy Tour'),
|
||||
('sansonnet', 'Maison Sansonnet'),
|
||||
('transquinnftw', 'transquinnftw')
|
||||
ON CONFLICT (slug) DO NOTHING;
|
||||
|
||||
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')
|
||||
ON CONFLICT (hostname) DO NOTHING;
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
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')
|
||||
`);
|
||||
|
||||
// ----- 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')
|
||||
`);
|
||||
}
|
||||
|
||||
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`);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue