diff --git a/source/SIL.AppBuilder.Portal/common/BullJobTypes.ts b/source/SIL.AppBuilder.Portal/common/BullJobTypes.ts index b77b401c3..943673d0d 100644 --- a/source/SIL.AppBuilder.Portal/common/BullJobTypes.ts +++ b/source/SIL.AppBuilder.Portal/common/BullJobTypes.ts @@ -9,6 +9,7 @@ export enum ScriptoriaJobType { Notify_Reviewers = 'Notify Reviewers', // Product Tasks Product_Create = 'Create Product', + Product_Delete = 'Delete Product', // Project Tasks Project_Create = 'Create Project', Project_Check = 'Check Project Creation', @@ -52,6 +53,11 @@ export namespace Product { type: ScriptoriaJobType.Product_Create; productId: string; } + export interface Delete { + type: ScriptoriaJobType.Product_Delete; + organizationId: number; + workflowJobId: number; + } } export namespace Project { @@ -147,6 +153,7 @@ export type JobTypeMap = { [ScriptoriaJobType.Build_Check]: Build.Check; [ScriptoriaJobType.Notify_Reviewers]: Notify.Reviewers; [ScriptoriaJobType.Product_Create]: Product.Create; + [ScriptoriaJobType.Product_Delete]: Product.Delete; [ScriptoriaJobType.Project_Create]: Project.Create; [ScriptoriaJobType.Project_Check]: Project.Check; [ScriptoriaJobType.Publish_Product]: Publish.Product; diff --git a/source/SIL.AppBuilder.Portal/common/build-engine-api/requests.ts b/source/SIL.AppBuilder.Portal/common/build-engine-api/requests.ts index 2716065e2..f2d2fb782 100644 --- a/source/SIL.AppBuilder.Portal/common/build-engine-api/requests.ts +++ b/source/SIL.AppBuilder.Portal/common/build-engine-api/requests.ts @@ -13,7 +13,7 @@ export async function request( headers: { Authorization: `Bearer ${token}`, Accept: 'application/json', - 'Content-Type': body? 'application/json' : undefined + 'Content-Type': body ? 'application/json' : undefined }, body: body ? JSON.stringify(body) : undefined }); @@ -71,9 +71,14 @@ export async function getProject( ? ((await res.json()) as Types.ProjectResponse) : ((await res.json()) as Types.ErrorResponse); } -export async function deleteProject(organizationId: number, projectId: number): Promise { +export async function deleteProject( + organizationId: number, + projectId: number +): Promise { const res = await request(`project/${projectId}`, organizationId, 'DELETE'); - return res.status; + return res.ok + ? { responseType: 'delete', status: res.status } + : ((await res.json()) as Types.ErrorResponse); } export async function getProjectAccessToken( @@ -113,9 +118,14 @@ export async function getJob( ? ((await res.json()) as Types.JobResponse) : ((await res.json()) as Types.ErrorResponse); } -export async function deleteJob(organizationId: number, jobId: number): Promise { +export async function deleteJob( + organizationId: number, + jobId: number +): Promise { const res = await request(`job/${jobId}`, organizationId, 'DELETE'); - return res.status; + return res.ok + ? { responseType: 'delete', status: res.status } + : ((await res.json()) as Types.ErrorResponse); } export async function createBuild( @@ -151,9 +161,11 @@ export async function deleteBuild( organizationId: number, jobId: number, buildId: number -): Promise { +): Promise { const res = await request(`job/${jobId}/build/${buildId}`, organizationId, 'DELETE'); - return res.status; + return res.ok + ? { responseType: 'delete', status: res.status } + : ((await res.json()) as Types.ErrorResponse); } export async function createRelease( @@ -183,11 +195,13 @@ export async function deleteRelease( jobId: number, buildId: number, releaseId: number -): Promise { +): Promise { const res = await request( `job/${jobId}/build/${buildId}/release/${releaseId}`, organizationId, 'DELETE' ); - return res.status; + return res.ok + ? { responseType: 'delete', status: res.status } + : ((await res.json()) as Types.ErrorResponse); } diff --git a/source/SIL.AppBuilder.Portal/common/build-engine-api/types.ts b/source/SIL.AppBuilder.Portal/common/build-engine-api/types.ts index 4aa5f4e6a..ca65693da 100644 --- a/source/SIL.AppBuilder.Portal/common/build-engine-api/types.ts +++ b/source/SIL.AppBuilder.Portal/common/build-engine-api/types.ts @@ -14,6 +14,11 @@ export type ErrorResponse = { type: string; }; +export type DeleteResponse = { + responseType: 'delete'; + status: number; +}; + type SuccessResponse = { responseType: 'project' | 'token' | 'job' | 'build' | 'release'; id: number; diff --git a/source/SIL.AppBuilder.Portal/common/databaseProxy/Products.ts b/source/SIL.AppBuilder.Portal/common/databaseProxy/Products.ts index a0111e1a1..0480d7d01 100644 --- a/source/SIL.AppBuilder.Portal/common/databaseProxy/Products.ts +++ b/source/SIL.AppBuilder.Portal/common/databaseProxy/Products.ts @@ -1,6 +1,7 @@ import type { Prisma } from '@prisma/client'; import prisma from '../prisma.js'; import { RequirePrimitive } from './utility.js'; +import { BullMQ, queues } from '../index.js'; export async function create( productData: RequirePrimitive @@ -43,12 +44,8 @@ export async function update( const productDefinitionId = productData.ProductDefinitionId ?? existing!.ProductDefinitionId; const storeId = productData.StoreId ?? existing!.StoreId; const storeLanguageId = productData.StoreLanguageId ?? existing!.StoreLanguageId; - if (!(await validateProductBase( - projectId, - productDefinitionId, - storeId, - storeLanguageId - ))) return false; + if (!(await validateProductBase(projectId, productDefinitionId, storeId, storeLanguageId))) + return false; // No additional verification steps @@ -66,8 +63,36 @@ export async function update( return true; } -function deleteProduct(productId: string) { +async function deleteProduct(productId: string) { // Delete all userTasks for this product, and delete the product + const product = await prisma.products.findUnique({ + where: { + Id: productId + }, + select: { + Project: { + select: { + OrganizationId: true + } + }, + WorkflowJobId: true + } + }); + queues.scriptoria.add( + `Delete Product #${productId} from BuildEngine`, + { + type: BullMQ.ScriptoriaJobType.Product_Delete, + organizationId: product.Project.OrganizationId, + workflowJobId: product.WorkflowJobId + }, + { + attempts: 5, + backoff: { + type: 'exponential', + delay: 5000 // 5 seconds + } + } + ); return prisma.$transaction([ prisma.workflowInstances.delete({ where: { @@ -85,7 +110,6 @@ function deleteProduct(productId: string) { } }) ]); - // TODO: delete from BuildEngine } export { deleteProduct as delete }; @@ -140,15 +164,17 @@ async function validateProductBase( // Store type must match Workflow store type Id: true, // StoreLanguage must be allowed by Store, if the StoreLanguage is defined - StoreLanguages: storeLanguageId === undefined || storeLanguageId === null ? - undefined : { - where: { - Id: storeLanguageId - }, - select: { - Id: true - } - } + StoreLanguages: + storeLanguageId === undefined || storeLanguageId === null + ? undefined + : { + where: { + Id: storeLanguageId + }, + select: { + Id: true + } + } } } } @@ -175,7 +201,8 @@ async function validateProductBase( // 2. The project has a WorkflowProjectUrl // handled by query // 4. The language is allowed by the store - (storeLanguageId ?? project.Organization.OrganizationStores[0].Store.StoreType.StoreLanguages.length > 0) && + (storeLanguageId ?? + project.Organization.OrganizationStores[0].Store.StoreType.StoreLanguages.length > 0) && // 5. The product type is allowed by the organization project.Organization.OrganizationProductDefinitions.length > 0 ); diff --git a/source/SIL.AppBuilder.Portal/node-server/BullWorker.ts b/source/SIL.AppBuilder.Portal/node-server/BullWorker.ts index c6b610523..07f8537d7 100644 --- a/source/SIL.AppBuilder.Portal/node-server/BullWorker.ts +++ b/source/SIL.AppBuilder.Portal/node-server/BullWorker.ts @@ -30,6 +30,8 @@ export class ScriptoriaWorker extends BullWorker { return new Executor.Notify.Reviewers().execute(job as JobCast); case BullMQ.ScriptoriaJobType.Product_Create: return new Executor.Product.Create().execute(job as JobCast); + case BullMQ.ScriptoriaJobType.Product_Delete: + return new Executor.Product.Delete().execute(job as JobCast); case BullMQ.ScriptoriaJobType.Project_Create: return new Executor.Project.Create().execute(job as JobCast); case BullMQ.ScriptoriaJobType.Project_Check: diff --git a/source/SIL.AppBuilder.Portal/node-server/job-executors/build.ts b/source/SIL.AppBuilder.Portal/node-server/job-executors/build.ts index caefad223..ad0f061ca 100644 --- a/source/SIL.AppBuilder.Portal/node-server/job-executors/build.ts +++ b/source/SIL.AppBuilder.Portal/node-server/job-executors/build.ts @@ -9,7 +9,6 @@ import { import { Job } from 'bullmq'; import { ScriptoriaJobExecutor } from './base.js'; -// TODO: What would be a meaningful return? export class Product extends ScriptoriaJobExecutor { async execute(job: Job): Promise { const productData = await prisma.products.findUnique({ @@ -37,7 +36,7 @@ export class Product extends ScriptoriaJobExecutor { async execute(job: Job): Promise { const productData = await prisma.products.findUnique({ @@ -43,7 +42,6 @@ export class Create extends ScriptoriaJobExecutor { + async execute(job: Job): Promise { + const response = await BuildEngine.Requests.deleteJob( + job.data.organizationId, + job.data.workflowJobId + ); + job.updateProgress(50); + if (response.responseType === 'error') { + job.log(response.message); + throw new Error(response.message); + } else { + job.updateProgress(100); + return response.status; + } + } +} diff --git a/source/SIL.AppBuilder.Portal/node-server/job-executors/project.ts b/source/SIL.AppBuilder.Portal/node-server/job-executors/project.ts index c11f62694..7f0a9a920 100644 --- a/source/SIL.AppBuilder.Portal/node-server/job-executors/project.ts +++ b/source/SIL.AppBuilder.Portal/node-server/job-executors/project.ts @@ -8,7 +8,6 @@ import { import { Job } from 'bullmq'; import { ScriptoriaJobExecutor } from './base.js'; -// TODO: What would be a meaningful return? export class Create extends ScriptoriaJobExecutor { async execute(job: Job): Promise { const projectData = await prisma.projects.findUnique({ diff --git a/source/SIL.AppBuilder.Portal/node-server/job-executors/publish.ts b/source/SIL.AppBuilder.Portal/node-server/job-executors/publish.ts index 9e9de54fb..91155483e 100644 --- a/source/SIL.AppBuilder.Portal/node-server/job-executors/publish.ts +++ b/source/SIL.AppBuilder.Portal/node-server/job-executors/publish.ts @@ -9,7 +9,6 @@ import { import { Job } from 'bullmq'; import { ScriptoriaJobExecutor } from './base.js'; -// TODO: What would be a meaningful return? export class Product extends ScriptoriaJobExecutor { async execute(job: Job): Promise { const productData = await prisma.products.findUnique({ @@ -40,7 +39,7 @@ export class Product extends ScriptoriaJobExecutor