Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Experiment Archive State #987

Merged
merged 23 commits into from
Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
34b6e10
working archive feature frontend changes
ppratikcr7 Aug 14, 2023
38caaeb
ignoring storing logs for archived experiments
ppratikcr7 Aug 21, 2023
b056ac9
add few more missing readonly properties for archive state
ppratikcr7 Aug 24, 2023
e2954e1
missed readonly for archive state
ppratikcr7 Aug 24, 2023
62ea73c
Create API and database for archive
Aug 26, 2023
bd8b669
resolved failing test cases for archive state
ppratikcr7 Aug 28, 2023
5259dff
Merge branch 'dev' of https://github.com/CarnegieLearningWeb/UpGrade …
ppratikcr7 Aug 28, 2023
f72c420
import change
ppratikcr7 Aug 28, 2023
d0a7763
not allowing archived state change from enrolling state
ppratikcr7 Aug 31, 2023
fba94cf
Merge branch 'dev' into feature/archive-status-backend-issue30
danoswaltCL Aug 31, 2023
e8b9913
Merge branch 'dev' into feature/archive-status-backend-issue30
danoswaltCL Sep 1, 2023
3be74d6
merge analysis and archieve api
Sep 11, 2023
06fcd6c
added remaining file
Sep 11, 2023
8bce729
fix peer review comments for archive state
ppratikcr7 Sep 12, 2023
6ac950d
Merge branch 'dev' into feature/archive-status-backend-issue30
ppratikcr7 Sep 12, 2023
b4d1b23
Merge branch 'dev' into feature/archive-status-backend-issue30
danoswaltCL Sep 14, 2023
5aaf70b
Merge branch 'dev' into feature/archive-status-backend-issue30
danoswaltCL Sep 19, 2023
57e3198
Merge branch 'dev' into feature/archive-status-backend-issue30
danoswaltCL Sep 20, 2023
d1a2d82
Solve review comments
Sep 22, 2023
03bfe20
Merge branch 'dev' into feature/archive-status-backend-issue30
ppratikcr7 Sep 29, 2023
be8bb4f
Merge branch 'dev' into feature/archive-status-backend-issue30
danoswaltCL Oct 16, 2023
8e40d34
Merge branch 'dev' into feature/archive-status-backend-issue30
ppratikcr7 Oct 26, 2023
f13630a
Merge branch 'dev' into feature/archive-status-backend-issue30
danoswaltCL Oct 26, 2023
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
18 changes: 18 additions & 0 deletions backend/packages/Upgrade/src/api/models/ArchivedStats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Entity, PrimaryColumn, Column, OneToOne, JoinColumn } from 'typeorm';
import { IsNotEmpty } from 'class-validator';
import { BaseModel } from './base/BaseModel';
import { Query } from './Query';

@Entity()
export class ArchivedStats extends BaseModel {
@PrimaryColumn('uuid')
public id: string;

@IsNotEmpty()
@Column('jsonb')
public result: object;

@OneToOne(() => Query, (query) => query.archivedStats, { onDelete: 'CASCADE' })
@JoinColumn()
public query: Query;
}
8 changes: 7 additions & 1 deletion backend/packages/Upgrade/src/api/models/Query.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Entity, ManyToOne, Column, PrimaryColumn } from 'typeorm';
import { Entity, ManyToOne, Column, PrimaryColumn, OneToOne } from 'typeorm';
import { BaseModel } from './base/BaseModel';
import { Metric } from './Metric';
import { Experiment } from './Experiment';
import { IsDefined } from 'class-validator';
import { REPEATED_MEASURE } from 'upgrade_types';
import { ArchivedStats } from './ArchivedStats';
import { Type } from 'class-transformer';

@Entity()
export class Query extends BaseModel {
Expand All @@ -23,6 +25,10 @@ export class Query extends BaseModel {
@ManyToOne(() => Experiment, (experiment) => experiment.queries, { onDelete: 'CASCADE' })
public experiment: Experiment;

@OneToOne(() => ArchivedStats, (archivedStats) => archivedStats.query, { onDelete: 'CASCADE' })
@Type(() => ArchivedStats)
public archivedStats: ArchivedStats;

@Column({
type: 'enum',
enum: REPEATED_MEASURE,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { EntityRepository, Repository } from 'typeorm';
import { ArchivedStats } from '../models/ArchivedStats';
import repositoryError from './utils/repositoryError';

@EntityRepository(ArchivedStats)
export class ArchivedStatsRepository extends Repository<ArchivedStats> {
public async saveRawJson(
rawDataArray: Array<Omit<ArchivedStats, 'createdAt' | 'updatedAt' | 'versionNumber'>>
): Promise<ArchivedStats> {
const result = await this.createQueryBuilder('ArchivedStats')
.insert()
.into(ArchivedStats)
.values(rawDataArray)
.onConflict(`DO NOTHING`)
.returning('*')
.execute()
.catch((errorMsg: any) => {
const errorMsgString = repositoryError(this.constructor.name, 'saveRawJson', { rawDataArray }, errorMsg);
throw errorMsgString;
});

return result.raw;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Metric } from '../models/Metric';
import { EntityRepository, Repository } from 'typeorm';
import repositoryError from './utils/repositoryError';
import { EXPERIMENT_STATE } from 'upgrade_types';

@EntityRepository(Metric)
export class MetricRepository extends Repository<Metric> {
Expand Down Expand Up @@ -32,7 +33,9 @@ export class MetricRepository extends Repository<Metric> {
public async findMetricsWithQueries(ids: string[]): Promise<Metric[]> {
VivekFitkariwala marked this conversation as resolved.
Show resolved Hide resolved
return this.createQueryBuilder('metrics')
.innerJoin('metrics.queries', 'queries')
.innerJoin('queries.experiment', 'experiment')
.where('key IN (:...ids)', { ids })
.andWhere('experiment.state NOT IN (:...archived)', { archived: [EXPERIMENT_STATE.ARCHIVED] })
.getMany()
.catch((errorMsg: any) => {
const errorMsgString = repositoryError(this.constructor.name, 'findMetricsWithQueries', { ids }, errorMsg);
Expand Down
27 changes: 25 additions & 2 deletions backend/packages/Upgrade/src/api/services/ExperimentService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ import {
import { ConditionPayloadDTO } from '../DTO/ConditionPayloadDTO';
import { FactorDTO } from '../DTO/FactorDTO';
import { LevelDTO } from '../DTO/LevelDTO';
import { QueryService } from './QueryService';
import { ArchivedStats } from '../models/ArchivedStats';
import { ArchivedStatsRepository } from '../repositories/ArchivedStatsRepository';

@Service()
export class ExperimentService {
Expand All @@ -93,10 +96,12 @@ export class ExperimentService {
@OrmRepository() private factorRepository: FactorRepository,
@OrmRepository() private levelRepository: LevelRepository,
@OrmRepository() private levelCombinationElementsRepository: LevelCombinationElementRepository,
@OrmRepository() private archivedStatsRepository: ArchivedStatsRepository,
public previewUserService: PreviewUserService,
public segmentService: SegmentService,
public scheduledJobService: ScheduledJobService,
public errorService: ErrorService
public errorService: ErrorService,
public queryService: QueryService
) {}

public async find(logger?: UpgradeLogger): Promise<ExperimentDTO[]> {
Expand Down Expand Up @@ -384,7 +389,7 @@ export class ExperimentService {
): Promise<Experiment> {
const oldExperiment = await this.experimentRepository.findOne(
{ id: experimentId },
{ relations: ['stateTimeLogs'] }
{ relations: ['stateTimeLogs', 'queries', 'queries.metric'] }
);

if (
Expand All @@ -394,6 +399,24 @@ export class ExperimentService {
await this.populateExclusionTable(experimentId, state, logger);
}

if (state === EXPERIMENT_STATE.ARCHIVED) {
const queryIds = oldExperiment.queries.map((query) => query.id);
const results = await this.queryService.analyze(queryIds, logger);
const archivedStatsData: Array<Omit<ArchivedStats, 'createdAt' | 'updatedAt' | 'versionNumber'>> = results.map(
(result) => {
const queryId = result.id;
delete result.id;
const archivedStats: Partial<ArchivedStats> = {
id: uuid(),
result: result,
query: queryId,
};
return archivedStats;
}
);
await this.archivedStatsRepository.saveRawJson(archivedStatsData);
}

let data: AuditLogData = {
experimentId,
experimentName: oldExperiment.name,
Expand Down
22 changes: 21 additions & 1 deletion backend/packages/Upgrade/src/api/services/QueryService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import { OrmRepository } from 'typeorm-typedi-extensions';
import { QueryRepository } from '../repositories/QueryRepository';
import { Query } from '../models/Query';
import { LogRepository } from '../repositories/LogRepository';
import { EXPERIMENT_TYPE, SERVER_ERROR } from 'upgrade_types';
import { EXPERIMENT_STATE, EXPERIMENT_TYPE, SERVER_ERROR } from 'upgrade_types';
import { ErrorService } from './ErrorService';
import { ExperimentError } from '../models/ExperimentError';
import { UpgradeLogger } from '../../lib/logger/UpgradeLogger';
import { Experiment } from '../models/Experiment';
import { ArchivedStatsRepository } from '../repositories/ArchivedStatsRepository';
import { In } from 'typeorm';

interface queryResult {
conditionId?: string;
Expand All @@ -21,6 +23,7 @@ export class QueryService {
constructor(
@OrmRepository() private queryRepository: QueryRepository,
@OrmRepository() private logRepository: LogRepository,
@OrmRepository() private archivedStatsRepository: ArchivedStatsRepository,
public errorService: ErrorService
) {}

Expand All @@ -35,6 +38,17 @@ export class QueryService {
});
}

public async getArchivedStats(queryIds: string[], logger: UpgradeLogger): Promise<any> {
logger.info({ message: `Get archivedStats of query with queryIds ${queryIds}` });
const archiveData = await this.archivedStatsRepository.find({
relations: ['query'],
where: { query: In(queryIds) },
});
return archiveData.map((data) => {
return { ...data.result, id: data.query.id };
});
}

public async analyze(queryIds: string[], logger: UpgradeLogger): Promise<any> {
logger.info({ message: `Get analysis of query with queryIds ${queryIds}` });
const promiseArray = queryIds.map((queryId) =>
Expand All @@ -52,6 +66,12 @@ export class QueryService {

const promiseResult = await Promise.all(promiseArray);
const experiments: Experiment[] = [];

// checks for archieve state experiment
if (promiseResult[0].experiment?.state === EXPERIMENT_STATE.ARCHIVED) {
return this.getArchivedStats(queryIds, logger);
}

const analyzePromise = promiseResult.map((query) => {
experiments.push(query.experiment);
if (query.experiment?.type === EXPERIMENT_TYPE.FACTORIAL) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class archivedState1692936809279 implements MigrationInterface {
name = 'archivedState1692936809279';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "archived_stats" ("createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "versionNumber" integer NOT NULL, "id" uuid NOT NULL, "result" jsonb NOT NULL, "queryId" uuid, CONSTRAINT "REL_df18fe5ea7298a1dc7bfb33b06" UNIQUE ("queryId"), CONSTRAINT "PK_afc8a335c8d9d48cefaf1aa2c09" PRIMARY KEY ("id"))`
);
await queryRunner.query(
`ALTER TYPE "public"."state_time_log_fromstate_enum" RENAME TO "state_time_log_fromstate_enum_old"`
);
await queryRunner.query(
`CREATE TYPE "public"."state_time_log_fromstate_enum" AS ENUM('inactive', 'preview', 'scheduled', 'enrolling', 'enrollmentComplete', 'cancelled', 'archived')`
);
await queryRunner.query(
`ALTER TABLE "public"."state_time_log" ALTER COLUMN "fromState" TYPE "public"."state_time_log_fromstate_enum" USING "fromState"::"text"::"public"."state_time_log_fromstate_enum"`
);
await queryRunner.query(`DROP TYPE "public"."state_time_log_fromstate_enum_old"`);
await queryRunner.query(
`ALTER TYPE "public"."state_time_log_tostate_enum" RENAME TO "state_time_log_tostate_enum_old"`
);
await queryRunner.query(
`CREATE TYPE "public"."state_time_log_tostate_enum" AS ENUM('inactive', 'preview', 'scheduled', 'enrolling', 'enrollmentComplete', 'cancelled', 'archived')`
);
await queryRunner.query(
`ALTER TABLE "public"."state_time_log" ALTER COLUMN "toState" TYPE "public"."state_time_log_tostate_enum" USING "toState"::"text"::"public"."state_time_log_tostate_enum"`
);
await queryRunner.query(`DROP TYPE "public"."state_time_log_tostate_enum_old"`);
await queryRunner.query(`ALTER TYPE "public"."experiment_state_enum" RENAME TO "experiment_state_enum_old"`);
await queryRunner.query(
`CREATE TYPE "public"."experiment_state_enum" AS ENUM('inactive', 'preview', 'scheduled', 'enrolling', 'enrollmentComplete', 'cancelled', 'archived')`
);
await queryRunner.query(`ALTER TABLE "public"."experiment" ALTER COLUMN "state" DROP DEFAULT`);
await queryRunner.query(
`ALTER TABLE "public"."experiment" ALTER COLUMN "state" TYPE "public"."experiment_state_enum" USING "state"::"text"::"public"."experiment_state_enum"`
);
await queryRunner.query(`ALTER TABLE "public"."experiment" ALTER COLUMN "state" SET DEFAULT 'inactive'`);
await queryRunner.query(`DROP TYPE "public"."experiment_state_enum_old"`);
await queryRunner.query(
`ALTER TABLE "archived_stats" ADD CONSTRAINT "FK_df18fe5ea7298a1dc7bfb33b06f" FOREIGN KEY ("queryId") REFERENCES "query"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "archived_stats" DROP CONSTRAINT "FK_df18fe5ea7298a1dc7bfb33b06f"`);
await queryRunner.query(
`CREATE TYPE "public"."experiment_state_enum_old" AS ENUM('cancelled', 'enrolling', 'enrollmentComplete', 'inactive', 'preview', 'scheduled')`
);
await queryRunner.query(`ALTER TABLE "public"."experiment" ALTER COLUMN "state" DROP DEFAULT`);
await queryRunner.query(
`ALTER TABLE "public"."experiment" ALTER COLUMN "state" TYPE "public"."experiment_state_enum_old" USING "state"::"text"::"public"."experiment_state_enum_old"`
);
await queryRunner.query(`ALTER TABLE "public"."experiment" ALTER COLUMN "state" SET DEFAULT 'inactive'`);
await queryRunner.query(`DROP TYPE "public"."experiment_state_enum"`);
await queryRunner.query(`ALTER TYPE "public"."experiment_state_enum_old" RENAME TO "experiment_state_enum"`);
await queryRunner.query(
`CREATE TYPE "public"."state_time_log_tostate_enum_old" AS ENUM('cancelled', 'enrolling', 'enrollmentComplete', 'inactive', 'preview', 'scheduled')`
);
await queryRunner.query(
`ALTER TABLE "public"."state_time_log" ALTER COLUMN "toState" TYPE "public"."state_time_log_tostate_enum_old" USING "toState"::"text"::"public"."state_time_log_tostate_enum_old"`
);
await queryRunner.query(`DROP TYPE "public"."state_time_log_tostate_enum"`);
await queryRunner.query(
`ALTER TYPE "public"."state_time_log_tostate_enum_old" RENAME TO "state_time_log_tostate_enum"`
);
await queryRunner.query(
`CREATE TYPE "public"."state_time_log_fromstate_enum_old" AS ENUM('cancelled', 'enrolling', 'enrollmentComplete', 'inactive', 'preview', 'scheduled')`
);
await queryRunner.query(
`ALTER TABLE "public"."state_time_log" ALTER COLUMN "fromState" TYPE "public"."state_time_log_fromstate_enum_old" USING "fromState"::"text"::"public"."state_time_log_fromstate_enum_old"`
);
await queryRunner.query(`DROP TYPE "public"."state_time_log_fromstate_enum"`);
await queryRunner.query(
`ALTER TYPE "public"."state_time_log_fromstate_enum_old" RENAME TO "state_time_log_fromstate_enum"`
);
await queryRunner.query(`DROP TABLE "archived_stats"`);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ afterEach(() => {
});

describe('MetricRepository Testing', () => {
it('should delete a metric', async () => {
it('should delete a metric', async () => {
createQueryBuilderStub = sandbox.stub(MetricRepository.prototype, 'createQueryBuilder').returns(deleteQueryBuilder);
const result = {
identifiers: [{ id: metric.key }],
Expand Down Expand Up @@ -106,8 +106,9 @@ describe('MetricRepository Testing', () => {
raw: [metric],
};

selectMock.expects('innerJoin').once().returns(selectQueryBuilder);
selectMock.expects('innerJoin').twice().returns(selectQueryBuilder);
selectMock.expects('where').once().returns(selectQueryBuilder);
selectMock.expects('andWhere').once().returns(selectQueryBuilder);
selectMock.expects('getMany').once().returns(Promise.resolve(result));

const res = await repo.findMetricsWithQueries([metric.key]);
Expand All @@ -121,8 +122,9 @@ describe('MetricRepository Testing', () => {
it('should throw an error when get monitored experiment metric by date range fails', async () => {
createQueryBuilderStub = sandbox.stub(MetricRepository.prototype, 'createQueryBuilder').returns(selectQueryBuilder);

selectMock.expects('innerJoin').once().returns(selectQueryBuilder);
selectMock.expects('innerJoin').twice().returns(selectQueryBuilder);
selectMock.expects('where').once().returns(selectQueryBuilder);
selectMock.expects('andWhere').once().returns(selectQueryBuilder);
selectMock.expects('getMany').once().returns(Promise.reject(err));

expect(async () => {
Expand Down
32 changes: 32 additions & 0 deletions backend/packages/Upgrade/test/unit/services/QueryService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@ import { Query } from '../../../src/api/models/Query';
import { Experiment } from '../../../src/api/models/Experiment';
import { ErrorService } from '../../../src/api/services/ErrorService';
import { ErrorRepository } from '../../../src/api/repositories/ErrorRepository';
import { ArchivedStatsRepository } from '../../../src/api/repositories/ArchivedStatsRepository';
import { ArchivedStats } from '../../../src/api/models/ArchivedStats';

const logger = new UpgradeLogger();

describe('Query Service Testing', () => {
let service: QueryService;
let queryRepo: Repository<QueryRepository>;
let archivedStatsRepo: Repository<ArchivedStatsRepository>;
let module: TestingModule;

const exp1 = new Experiment();
Expand Down Expand Up @@ -43,12 +46,19 @@ describe('Query Service Testing', () => {
result: 0,
participantsLogged: 0,
};

const mockArchiveData = new ArchivedStats();
mockArchiveData.id = 'id1';
mockArchiveData.query = mockquery1;
mockArchiveData.result = { mainEffect: logResult, interactionEffect: null };

beforeEach(async () => {
module = await Test.createTestingModule({
providers: [
QueryService,
QueryRepository,
LogRepository,
ArchivedStatsRepository,
ErrorService,
ErrorRepository,
{
Expand All @@ -69,6 +79,12 @@ describe('Query Service Testing', () => {
}),
},
},
{
provide: getRepositoryToken(ArchivedStatsRepository),
useValue: {
find: jest.fn().mockResolvedValue([mockArchiveData]),
},
},
{
provide: ErrorService,
useValue: {
Expand All @@ -80,6 +96,7 @@ describe('Query Service Testing', () => {

service = module.get<QueryService>(QueryService);
queryRepo = module.get<Repository<QueryRepository>>(getRepositoryToken(QueryRepository));
archivedStatsRepo = module.get<Repository<ArchivedStatsRepository>>(getRepositoryToken(ArchivedStatsRepository));
});

it('should be defined', async () => {
Expand All @@ -90,6 +107,10 @@ describe('Query Service Testing', () => {
expect(await queryRepo.find()).toEqual(queryArr);
});

it('should have the repo mocked', async () => {
expect(await archivedStatsRepo.find()).toEqual([mockArchiveData]);
});

it('should find and map queries to experiments', async () => {
const response = await service.find(logger);
expect(response).toEqual([
Expand Down Expand Up @@ -134,4 +155,15 @@ describe('Query Service Testing', () => {
},
]);
});

it('should find and map archivedStats to queries', async () => {
const response = await service.getArchivedStats(['id1'], logger);
expect(response).toEqual([
{
id: mockquery1.id,
interactionEffect: null,
mainEffect: logResult,
},
]);
});
});
Loading
Loading