Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

/**
* Issue #1244 — Add database indexes to the consumer-privacy-preference entity.
*
* Common query paths that need coverage:
* - "get user preferences" → WHERE "userId" = $1
* - "get tenant preferences list" → WHERE "tenantId" = $1
* - "get specific user within tenant" → WHERE "tenantId" = $1 AND "userId" = $2 (composite)
* - "users who opted out of data sale" → WHERE "doNotSellMyPersonalInformation" = true
* - "recent CCPA requests" → ORDER BY "lastCcpRequestDate" DESC / range queries
*
* Indexes added:
* - IDX_consumer_privacy_preferences_user_id — per-user lookup (FK column)
* - IDX_consumer_privacy_preferences_tenant_id — per-tenant filtering
* - IDX_consumer_privacy_preferences_tenant_user — composite tenant+user (unique, covers most common multi-tenant query)
* - IDX_consumer_privacy_preferences_do_not_sell — do-not-sell opt-out flag filtering
* - IDX_consumer_privacy_preferences_last_request_date — last CCPA request sorting/range queries
*/
export class AddConsumerPrivacyPreferenceIndexes1806000000000 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS "consumer_privacy_preferences" (
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
"version" integer NOT NULL DEFAULT 1,
"tenantId" character varying,
"userId" character varying NOT NULL,
"doNotSellMyPersonalInformation" boolean NOT NULL DEFAULT false,
"optOutOfDataSharing" boolean NOT NULL DEFAULT false,
"optOutOfMarketing" boolean NOT NULL DEFAULT false,
"limitUseOfSensitivePersonalInformation" boolean NOT NULL DEFAULT false,
"dataCategoryPreferences" jsonb,
"purposePreferences" jsonb,
"lastCcpRequestDate" TIMESTAMP,
"expiresAt" TIMESTAMP,
"source" character varying,
"identityVerified" boolean NOT NULL DEFAULT false,
"verificationMethod" character varying,
"notes" text,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP NOT NULL DEFAULT now(),
CONSTRAINT "PK_consumer_privacy_preferences" PRIMARY KEY ("id")
)
`);

await queryRunner.query(
'CREATE INDEX IF NOT EXISTS "IDX_consumer_privacy_preferences_user_id" ON "consumer_privacy_preferences" ("userId")',
);
await queryRunner.query(
'CREATE INDEX IF NOT EXISTS "IDX_consumer_privacy_preferences_tenant_id" ON "consumer_privacy_preferences" ("tenantId")',
);
await queryRunner.query(
'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_consumer_privacy_preferences_tenant_user" ON "consumer_privacy_preferences" ("tenantId", "userId")',
);
await queryRunner.query(
'CREATE INDEX IF NOT EXISTS "IDX_consumer_privacy_preferences_do_not_sell" ON "consumer_privacy_preferences" ("doNotSellMyPersonalInformation")',
);
await queryRunner.query(
'CREATE INDEX IF NOT EXISTS "IDX_consumer_privacy_preferences_last_request_date" ON "consumer_privacy_preferences" ("lastCcpRequestDate")',
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'DROP INDEX IF EXISTS "IDX_consumer_privacy_preferences_last_request_date"',
);
await queryRunner.query('DROP INDEX IF EXISTS "IDX_consumer_privacy_preferences_do_not_sell"');
await queryRunner.query('DROP INDEX IF EXISTS "IDX_consumer_privacy_preferences_tenant_user"');
await queryRunner.query('DROP INDEX IF EXISTS "IDX_consumer_privacy_preferences_tenant_id"');
await queryRunner.query('DROP INDEX IF EXISTS "IDX_consumer_privacy_preferences_user_id"');
}
}
82 changes: 82 additions & 0 deletions src/modules/ccpa/entities/consumer-privacy-preference.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
OneToOne,
JoinColumn,
CreateDateColumn,
UpdateDateColumn,
VersionColumn,
Index,
} from 'typeorm';
import { User } from '../../../users/entities/user.entity';

/**
* Represents a consumer's CCPA privacy preferences.
* Tracks opt-outs for data sale, marketing, and other CCPA rights.
*/
@Entity('consumer_privacy_preferences')
@Index('IDX_consumer_privacy_preferences_tenant_user', ['tenantId', 'userId'], { unique: true })
export class ConsumerPrivacyPreference {
@PrimaryGeneratedColumn('uuid')
id: string;

@VersionColumn()
version: number;

@Column({ nullable: true })
@Index('IDX_consumer_privacy_preferences_tenant_id')
tenantId?: string;

@Column()
@Index('IDX_consumer_privacy_preferences_user_id')
userId: string;

@OneToOne(() => User, { onDelete: 'CASCADE' })
@JoinColumn()
user: User;

@Column({ default: false })
@Index('IDX_consumer_privacy_preferences_do_not_sell')
doNotSellMyPersonalInformation: boolean;

@Column({ default: false })
optOutOfDataSharing: boolean;

@Column({ default: false })
optOutOfMarketing: boolean;

@Column({ default: false })
limitUseOfSensitivePersonalInformation: boolean;

@Column({ type: 'jsonb', nullable: true })
dataCategoryPreferences: Record<string, boolean>;

@Column({ type: 'jsonb', nullable: true })
purposePreferences: Record<string, boolean>;

@Column({ type: 'timestamp', nullable: true })
@Index('IDX_consumer_privacy_preferences_last_request_date')
lastCcpRequestDate: Date;

@Column({ type: 'timestamp', nullable: true })
expiresAt: Date;

@Column({ type: 'varchar', nullable: true })
source: string;

@Column({ default: false })
identityVerified: boolean;

@Column({ type: 'varchar', nullable: true })
verificationMethod: string;

@Column({ type: 'text', nullable: true })
notes: string;

@CreateDateColumn()
createdAt: Date;

@UpdateDateColumn()
updatedAt: Date;
}
Loading