Skip to content

Commit

Permalink
feat: script runner part 1 (#4092)
Browse files Browse the repository at this point in the history
* feat: script runner beginings

* feat: adding tests and update to service

* fix: updates per eric
  • Loading branch information
YazeedLoonat authored May 21, 2024
1 parent a324a17 commit 24be7e8
Show file tree
Hide file tree
Showing 9 changed files with 303 additions and 1 deletion.
14 changes: 14 additions & 0 deletions api/prisma/migrations/11_add_script_runs/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
-- CreateTable
CREATE TABLE "script_runs" (
"id" UUID NOT NULL DEFAULT uuid_generate_v4(),
"created_at" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(6) NOT NULL,
"script_name" TEXT NOT NULL,
"triggering_user" UUID NOT NULL,
"did_script_run" BOOLEAN NOT NULL DEFAULT false,

CONSTRAINT "script_runs_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "script_runs_script_name_key" ON "script_runs"("script_name");
11 changes: 11 additions & 0 deletions api/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -880,6 +880,17 @@ model UserPreferences {

// END DETROIT SPECIFIC

model ScriptRuns {
id String @id() @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(6)
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamp(6)
scriptName String @unique() @map("script_name")
triggeringUser String @map("triggering_user") @db.Uuid
didScriptRun Boolean @default(false) @map("did_script_run")
@@map("script_runs")
}

enum ApplicationMethodsTypeEnum {
Internal
FileDownload
Expand Down
33 changes: 33 additions & 0 deletions api/src/controllers/script-runner.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {
Controller,
Put,
Request,
UseGuards,
UsePipes,
ValidationPipe,
} from '@nestjs/common';
import { Request as ExpressRequest } from 'express';
import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';
import { ScriptRunnerService } from '../services/script-runner.service';
import { defaultValidationPipeOptions } from '../utilities/default-validation-pipe-options';
import { SuccessDTO } from '../dtos/shared/success.dto';
import { OptionalAuthGuard } from '../guards/optional.guard';
import { AdminOrJurisdictionalAdminGuard } from '../guards/admin-or-jurisdiction-admin.guard';

@Controller('scriptRunner')
@ApiTags('scriptRunner')
@UsePipes(new ValidationPipe(defaultValidationPipeOptions))
@UseGuards(OptionalAuthGuard, AdminOrJurisdictionalAdminGuard)
export class ScirptRunnerController {
constructor(private readonly scriptRunnerService: ScriptRunnerService) {}

@Put('exampleScript')
@ApiOperation({
summary: 'An example of how the script runner can work',
operationId: 'exampleScript',
})
@ApiOkResponse({ type: SuccessDTO })
async update(@Request() req: ExpressRequest): Promise<SuccessDTO> {
return await this.scriptRunnerService.example(req);
}
}
3 changes: 3 additions & 0 deletions api/src/modules/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { MapLayerModule } from './map-layer.module';
import { APP_GUARD } from '@nestjs/core';
import { ThrottlerModule } from '@nestjs/throttler';
import { ThrottleGuard } from '../guards/throttler.guard';
import { ScirptRunnerModule } from './script-runner.module';

@Module({
imports: [
Expand All @@ -38,6 +39,7 @@ import { ThrottleGuard } from '../guards/throttler.guard';
AuthModule,
ApplicationFlaggedSetModule,
MapLayerModule,
ScirptRunnerModule,
ThrottlerModule.forRoot([
{
ttl: Number(process.env.THROTTLE_TTL),
Expand Down Expand Up @@ -71,6 +73,7 @@ import { ThrottleGuard } from '../guards/throttler.guard';
AuthModule,
ApplicationFlaggedSetModule,
MapLayerModule,
ScirptRunnerModule,
],
})
export class AppModule {}
13 changes: 13 additions & 0 deletions api/src/modules/script-runner.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { ScirptRunnerController } from '../controllers/script-runner.controller';
import { ScriptRunnerService } from '../services/script-runner.service';
import { PrismaModule } from './prisma.module';
import { PermissionModule } from './permission.module';

@Module({
imports: [PrismaModule, PermissionModule],
controllers: [ScirptRunnerController],
providers: [ScriptRunnerService],
exports: [ScriptRunnerService],
})
export class ScirptRunnerModule {}
87 changes: 87 additions & 0 deletions api/src/services/script-runner.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { Injectable, BadRequestException } from '@nestjs/common';
import { Request as ExpressRequest } from 'express';
import { PrismaService } from './prisma.service';
import { SuccessDTO } from '../dtos/shared/success.dto';
import { User } from '../dtos/users/user.dto';
import { mapTo } from '../utilities/mapTo';

/**
this is the service for running scripts
most functions in here will be unique, but each function should only be allowed to fire once
*/
@Injectable()
export class ScriptRunnerService {
constructor(private prisma: PrismaService) {}

/**
this is simply an example
*/
async example(req: ExpressRequest): Promise<SuccessDTO> {
const requestingUser = mapTo(User, req['user']);
await this.markScriptAsRunStart('example', requestingUser);
const rawJurisdictions = await this.prisma.jurisdictions.findMany();
await this.markScriptAsComplete('example', requestingUser);
return { success: !!rawJurisdictions.length };
}

// |------------------ HELPERS GO BELOW ------------------ | //

/**
*
* @param scriptName the name of the script that is going to be run
* @param userTriggeringTheRun the user that is attempting to trigger the script run
* @description this checks to see if the script has already ran, if not marks the script in the db
*/
async markScriptAsRunStart(
scriptName: string,
userTriggeringTheRun: User,
): Promise<void> {
// check to see if script is already ran in db
const storedScriptRun = await this.prisma.scriptRuns.findUnique({
where: {
scriptName,
},
});

if (storedScriptRun?.didScriptRun) {
// if script run has already successfully completed throw already succeed error
throw new BadRequestException(
`${scriptName} has already been run and succeeded`,
);
} else if (storedScriptRun?.didScriptRun === false) {
// if script run was attempted but failed, throw attempt already failed error
throw new BadRequestException(
`${scriptName} has an attempted run and it failed, or is in progress. If it failed, please delete the db entry and try again`,
);
} else {
// if no script run has been attempted create script run entry
await this.prisma.scriptRuns.create({
data: {
scriptName,
triggeringUser: userTriggeringTheRun.id,
},
});
}
}

/**
*
* @param scriptName the name of the script that is going to be run
* @param userTriggeringTheRun the user that is setting the script run as successfully completed
* @description this marks the script run entry in the db as successfully completed
*/
async markScriptAsComplete(
scriptName: string,
userTriggeringTheRun: User,
): Promise<void> {
await this.prisma.scriptRuns.update({
data: {
didScriptRun: true,
triggeringUser: userTriggeringTheRun.id,
},
where: {
scriptName,
},
});
}
}
1 change: 0 additions & 1 deletion api/test/unit/services/app.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { randomUUID } from 'crypto';
import { Test, TestingModule } from '@nestjs/testing';
import { Logger } from '@nestjs/common';
import { SchedulerRegistry } from '@nestjs/schedule';
import { randomUUID } from 'crypto';
import { AppService } from '../../../src/services/app.service';
import { PrismaService } from '../../../src/services/prisma.service';

Expand Down
123 changes: 123 additions & 0 deletions api/test/unit/services/script-runner.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { Test, TestingModule } from '@nestjs/testing';
import { Logger } from '@nestjs/common';
import { randomUUID } from 'crypto';
import { SchedulerRegistry } from '@nestjs/schedule';
import { ScriptRunnerService } from '../../../src/services/script-runner.service';
import { PrismaService } from '../../../src/services/prisma.service';
import { User } from '../../../src/dtos/users/user.dto';

describe('Testing script runner service', () => {
let service: ScriptRunnerService;
let prisma: PrismaService;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ScriptRunnerService,
PrismaService,
Logger,
SchedulerRegistry,
],
}).compile();

service = module.get<ScriptRunnerService>(ScriptRunnerService);
prisma = module.get<PrismaService>(PrismaService);
});

// | ---------- HELPER TESTS BELOW ---------- | //
it('should mark script run as started if no script run present in db', async () => {
prisma.scriptRuns.findUnique = jest.fn().mockResolvedValue(null);
prisma.scriptRuns.create = jest.fn().mockResolvedValue(null);

const id = randomUUID();
const scriptName = 'new run attempt';

await service.markScriptAsRunStart(scriptName, {
id,
} as unknown as User);

expect(prisma.scriptRuns.findUnique).toHaveBeenCalledWith({
where: {
scriptName,
},
});
expect(prisma.scriptRuns.create).toHaveBeenCalledWith({
data: {
scriptName,
triggeringUser: id,
},
});
});

it('should error if script run is in progress or failed', async () => {
prisma.scriptRuns.findUnique = jest.fn().mockResolvedValue({
id: randomUUID(),
didScriptRun: false,
});
prisma.scriptRuns.create = jest.fn().mockResolvedValue(null);

const id = randomUUID();
const scriptName = 'new run attempt 2';

await expect(
async () =>
await service.markScriptAsRunStart(scriptName, {
id,
} as unknown as User),
).rejects.toThrowError(
`${scriptName} has an attempted run and it failed, or is in progress. If it failed, please delete the db entry and try again`,
);

expect(prisma.scriptRuns.findUnique).toHaveBeenCalledWith({
where: {
scriptName,
},
});
expect(prisma.scriptRuns.create).not.toHaveBeenCalled();
});

it('should error if script run already succeeded', async () => {
prisma.scriptRuns.findUnique = jest.fn().mockResolvedValue({
id: randomUUID(),
didScriptRun: true,
});
prisma.scriptRuns.create = jest.fn().mockResolvedValue(null);

const id = randomUUID();
const scriptName = 'new run attempt 3';

await expect(
async () =>
await service.markScriptAsRunStart(scriptName, {
id,
} as unknown as User),
).rejects.toThrowError(`${scriptName} has already been run and succeeded`);

expect(prisma.scriptRuns.findUnique).toHaveBeenCalledWith({
where: {
scriptName,
},
});
expect(prisma.scriptRuns.create).not.toHaveBeenCalled();
});

it('should mark script run as started if no script run present in db', async () => {
prisma.scriptRuns.update = jest.fn().mockResolvedValue(null);

const id = randomUUID();
const scriptName = 'new run attempt 4';

await service.markScriptAsComplete(scriptName, {
id,
} as unknown as User);

expect(prisma.scriptRuns.update).toHaveBeenCalledWith({
data: {
didScriptRun: true,
triggeringUser: id,
},
where: {
scriptName,
},
});
});
});
19 changes: 19 additions & 0 deletions shared-helpers/src/types/backend-swagger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1995,6 +1995,25 @@ export class MapLayersService {
}
}

export class ScriptRunnerService {
/**
* An example of how the script runner can work
*/
exampleScript(options: IRequestOptions = {}): Promise<SuccessDTO> {
return new Promise((resolve, reject) => {
let url = basePath + "/scriptRunner/exampleScript"

const configs: IRequestConfig = getConfigs("put", "application/json", url, options)

let data = null

configs.data = data

axios(configs, resolve, reject)
})
}
}

export interface SuccessDTO {
/** */
success: boolean
Expand Down

0 comments on commit 24be7e8

Please sign in to comment.