Skip to content
11 changes: 9 additions & 2 deletions src/ab-testing/entities/experiment.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
UpdateDateColumn,
OneToMany,
VersionColumn,
Index,
} from 'typeorm';
import { IExperimentVariant } from './experiment-variant.entity';
import { ExperimentMetric } from './experiment-metric.entity';
Expand Down Expand Up @@ -39,23 +40,27 @@ export class Experiment {
@Column({ type: 'text' })
description: string;

@Index('IDX_experiments_type')
@Column({
type: 'enum',
enum: ExperimentType,
default: ExperimentType.A_B_TEST,
})
type: ExperimentType;

@Index('IDX_experiments_status')
@Column({
type: 'enum',
enum: ExperimentStatus,
default: ExperimentStatus.DRAFT,
})
status: ExperimentStatus;

@Index('IDX_experiments_start_date')
@Column({ type: 'timestamp' })
startDate: Date;

@Index('IDX_experiments_end_date')
@Column({ type: 'timestamp', nullable: true })
endDate?: Date;

Expand Down Expand Up @@ -83,15 +88,17 @@ export class Experiment {
@Column({ type: 'json', nullable: true })
properties?: Record<string, any>;

@Index('IDX_experiments_created_at')
@CreateDateColumn()
createdAt: Date;

@Index('IDX_experiments_updated_at')
@UpdateDateColumn()
updatedAt: Date;

@OneToMany(() => IExperimentVariant, (variant) => variant.experiment)
@OneToMany()=> IExperimentVariant, (variant) => variant.experiment)
variants: IExperimentVariant[];

@OneToMany(() => ExperimentMetric, (metric) => metric.experiment)
@OneToMany()=> ExperimentMetric, (metric) => metric.experiment)
metrics: ExperimentMetric[];
}
1 change: 0 additions & 1 deletion src/achievements/achievements.seed.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { AchievementType, AchievementDifficulty } from './entities/achievement.entity';
import { Logger } from '@nestjs/common';

/**
* Seed data for default achievements
Expand Down
17 changes: 8 additions & 9 deletions src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import { RefreshTokenDto } from './dto/refresh-token.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from '../users/entities/user.entity';
import { Tenant } from '../tenancy/entities/tenant.entity';
import { Repository } from 'typeorm';
import * as bcrypt from 'bcrypt';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
Expand All @@ -44,7 +43,7 @@
@HttpCode(HttpStatus.CREATED)
@Throttle({ default: THROTTLE.STRICT })
@LimitType('user')
@UseGuards(TenantLimitGuard)
@useGuards(TenantLimitGuard)
@ApiOperation({ summary: 'Register a new user account' })
@ApiResponse({ status: 201, description: 'User registered successfully' })
@ApiResponse({ status: 402, description: 'Tenant user limit exceeded' })
Expand Down Expand Up @@ -113,11 +112,11 @@
}

@Post('login')
@HttpCode(HttpStatus.OK)
@Throttle({ default: THROTTLE.AUTH_LOGIN })
@ApiOperation({ summary: 'Log in with email and password' })
@ApiResponse({ status: 200, description: 'Successfully authenticated' })
@ApiResponse({ status: 401, description: 'Invalid credentials' })
@HttpCode(HttpStatus.OK)

Check failure on line 115 in src/auth/auth.controller.ts

View workflow job for this annotation

GitHub Actions / validate

Delete `·`
@Throttle({ default: THROTTLE.AUTH_LOGIN })

Check failure on line 116 in src/auth/auth.controller.ts

View workflow job for this annotation

GitHub Actions / validate

Delete `·`
@ApiOperation({ summary: 'Log in with email and password' })

Check failure on line 117 in src/auth/auth.controller.ts

View workflow job for this annotation

GitHub Actions / validate

Delete `·`
@ApiResponse({ status: 200, description: 'Successfully authenticated' })

Check failure on line 118 in src/auth/auth.controller.ts

View workflow job for this annotation

GitHub Actions / validate

Delete `·`
@ApiResponse({ status: 401, description: 'Invalid credentials' })

Check failure on line 119 in src/auth/auth.controller.ts

View workflow job for this annotation

GitHub Actions / validate

Delete `·`
async login(@Body() loginDto: LoginDto, @Req() req: any) {
const user = await this.userRepository.findOne({
where: { email: loginDto.email },
Expand Down Expand Up @@ -162,7 +161,7 @@
// If MFA is not enabled but is enforced for this role, we can either:
// 1. Issue a token and expect the frontend to force them to /mfa/setup (most common in APIs where the frontend checks isMfaEnabled)
// 2. Reject the login. But rejecting the login means they can never authenticate to set it up.
// We will allow login here, but they must set it up.
// We will sellow login here, but they must set it up.
// The requirement "Admin login without valid TOTP returns 401" will be covered when isMfaEnabled = true.
// Alternatively, we could require a pre-auth setup flow. We'll issue the token for now.
}
Expand All @@ -187,7 +186,7 @@
}

@Post('logout')
@UseGuards(JwtAuthGuard)
@useGuards(JwtAuthGuard)
@Throttle({ default: THROTTLE.AUTH_DEFAULT })
@ApiBearerAuth()
@HttpCode(HttpStatus.OK)
Expand Down
1 change: 0 additions & 1 deletion src/email-marketing/automation/automation.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,6 @@ export class AutomationService {
);
break;
default:
// eslint-disable-next-line no-console -- warn on unhandled automation action type
this.logger.warn(`Unknown action type: ${action.type}`);
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/migrations/1599999999999-BaselineSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2105,6 +2105,8 @@ export class BaselineSchema1599999999999 implements MigrationInterface {
`);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_e865d6f59667897a7fb67ff69a" ON public.user_quota_usage USING btree ("userId", period)
`);
await queryRunner.query(`CREATE INDEX "IDX_experiments_status_start_date_end_date" ON public.experiments USING btree (status, "startDate", "endDate")
`);
await queryRunner.query(`CREATE INDEX idx_notifications_dedup ON public.notifications USING btree ("userId", type, content_hash, "createdAt")
`);
await queryRunner.query(`ALTER TABLE ONLY public.invoices
Expand Down
27 changes: 27 additions & 0 deletions src/migrations/1735689600-add-experiment-indexes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AddExperimentIndexes1735689600 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('CREATE INDEX "IDX_experiments_status" ON "experiments" ("status")');
await queryRunner.query('CREATE INDEX "IDX_experiments_type" ON "experiments" ("type")');
await queryRunner.query(
'CREATE INDEX "IDX_experiments_start_date" ON "experiments" ("startDate")',
);
await queryRunner.query('CREATE INDEX "IDX_experiments_end_date" ON "experiments" ("endDate")');
await queryRunner.query(
'CREATE INDEX "IDX_experiments_created_at" ON "experiments" ("createdAt")',
);
await queryRunner.query(
'CREATE INDEX "IDX_experiments_updated_at" ON "experiments" ("updatedAt")',
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('DROP INDEX "IDX_experiments_status"');
await queryRunner.query('DROP INDEX "IDX_experiments_type"');
await queryRunner.query('DROP INDEX "IDX_experiments_start_date"');
await queryRunner.query('DROP INDEX "IDX_experiments_end_date");
await queryRunner.query('DROP INDEX "IDX_experiments_created_at"');
await queryRunner.query('DROP INDEX "IDX_experiments_updated_at"');
}
}
69 changes: 69 additions & 0 deletions src/migrations/1756475492000-add-experiment-indexes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { MigrationInterface, QueryRunner, TableIndex } from 'typeorm';

/**
* Adds indexes covering the common lookup / filter / sort / foreign-key
* columns on the `experiment` entity.
*
* Indexes created:
* - IDX_experiment_status : filtering by status (WHERE status = ...)
* - IDX_experiment_projectId : foreign-key lookups / filtering by project
* - IDX_experiment_createdById : foreign-key lookups by owner/creator
* - IDX_experiment_createdAt : sorting (ORDER BY createdAt)
* - IDX_experiment_project_status : composite for the common
* "experiments for a project by status" path
*/
export class AddExperimentIndexes1756475492000 implements MigrationInterface {
name = 'AddExperimentIndexes1756475492000';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createIndex(
'experiment',
new TableIndex({
name: 'IDX_experiment_status',
columnNames: ['status'],
}),
);

await queryRunner.createIndex(
'experiment',
new TableIndex({
name: 'IDX_experiment_projectId',
columnNames: ['projectId'],
}),
);

await queryRunner.createIndex(
'experiment',
new TableIndex({
name: 'IDX_experiment_createdById',
columnNames: ['createdById'],
}),
);

await queryRunner.createIndex(
'experiment',
new TableIndex({
name: 'IDX_experiment_createdAt',
columnNames: ['createdAt'],
}),
);

// Composite index for the frequent "list a project's experiments
// filtered by status" query path.
await queryRunner.createIndex(
'experiment',
new TableIndex({
name: 'IDX_experiment_project_status',
columnNames: ['projectId', 'status'],
}),
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropIndex('experiment', 'IDX_experiment_project_status');
await queryRunner.dropIndex('experiment', 'IDX_experiment_createdAt');
await queryRunner.dropIndex('experiment', 'IDX_experiment_createdById');
await queryRunner.dropIndex('experiment', 'IDX_experiment_projectId');
await queryRunner.dropIndex('experiment', 'IDX_experiment_status');
}
}
6 changes: 3 additions & 3 deletions src/payments/providers/payment-provider.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export interface CreditResult {
* Issue #1007 — extracted so SubscriptionsService can inject it and
* tests can mock it without touching the real provider.
*/
@Injectable()
@Injectable
export class PaymentProviderService {
private readonly logger = new Logger(PaymentProviderService.name);

Expand All @@ -38,7 +38,7 @@ export class PaymentProviderService {
userId: string,
amount: number,
currency: string,
metadata: Record<string, unknown> = {},
_metadata: Record<string, unknown> = {},
): Promise<ChargeResult> {
// TODO: replace with real Stripe call:
// const intent = await stripe.paymentIntents.create({ amount: Math.round(amount * 100), currency, ... });
Expand All @@ -58,7 +58,7 @@ export class PaymentProviderService {
userId: string,
amount: number,
currency: string,
metadata: Record<string, unknown> = {},
_metadata: Record<string, unknown> = {},
): Promise<CreditResult> {
// TODO: replace with real Stripe call:
// const credit = await stripe.customers.createBalanceTransaction(customerId, { amount: -Math.round(amount * 100), currency });
Expand Down
Loading