diff --git a/api/prisma/schema.prisma b/api/prisma/schema.prisma index 3abb9f08..bb55ca49 100644 --- a/api/prisma/schema.prisma +++ b/api/prisma/schema.prisma @@ -66,27 +66,28 @@ model Document { documentType String syncronizationUnits SyncronizationUnit[] state String? + @@id([id, driveId]) } model Operation { - id String @id @default(uuid()) - driveId String - Document Document? @relation(fields: [driveId, documentId], references: [driveId, id], onDelete: Cascade) - documentId String - scope String - branch String - index Int - skip Int - hash String - timestamp DateTime - input Json - type String - attachments Attachment[] - syncId String? - clipboard Boolean? @default(false) - context Json? - resultingState String? + id String @id @default(uuid()) + driveId String + Document Document? @relation(fields: [driveId, documentId], references: [driveId, id], onDelete: Cascade) + documentId String + scope String + branch String + index Int + skip Int + hash String + timestamp DateTime + input Json + type String + attachments Attachment[] + syncId String? + clipboard Boolean? @default(false) + context Json? + resultingState String? SyncronizationUnit SyncronizationUnit? @relation(fields: [syncId, driveId], references: [id, driveId], onDelete: Cascade) @@unique([driveId, documentId, scope, branch, index], name: "unique_operation") @@ -331,13 +332,14 @@ model RWABaseTransactionOnGroupTransaction { } model ScopeOfWorkDeliverable { - driveId String - documentId String - id String - title String - description String - status String + driveId String + documentId String + id String + title String + description String + status String githubCreated Boolean + githubId Int? @@id([id, driveId, documentId]) -} \ No newline at end of file +} diff --git a/api/src/app.ts b/api/src/app.ts index b79eb909..b333f278 100644 --- a/api/src/app.ts +++ b/api/src/app.ts @@ -8,6 +8,7 @@ import { import * as Sentry from "@sentry/node"; import { nodeProfilingIntegration } from "@sentry/profiling-node"; import bodyParser from 'body-parser'; +import prisma from './database'; const logger = getChildLogger({ msgPrefix: 'APP' }); @@ -78,6 +79,20 @@ export const createApp = (): { app: Express, router: express.Router } => { } ); + // Hooks + router.post('/h/github', async (req, res) => { + const issueId = req.body?.issue?.number; + if (!issueId) { + throw new Error('Issue number not found in request body') + } + + const result = await prisma.document.closeScopeOfWorkIssue(req.body.issue.number) + if (!result) { + throw new Error('Failed to close issue') + } + res.sendStatus(200).send(result); + }); + const basePath = process.env.BASE_PATH || '/'; app.use(basePath, router); diff --git a/api/src/modules/document/model.ts b/api/src/modules/document/model.ts index 47662204..a6b279c8 100644 --- a/api/src/modules/document/model.ts +++ b/api/src/modules/document/model.ts @@ -1,11 +1,11 @@ import type { Prisma, PrismaClient } from '@prisma/client'; import { - DocumentDriveServer, - DriveInput, - ListenerRevision, - StrandUpdate, - generateUUID, - PullResponderTransmitter, + DocumentDriveServer, + DriveInput, + ListenerRevision, + StrandUpdate, + generateUUID, + PullResponderTransmitter, } from 'document-drive'; import { ILogger, setLogger } from 'document-drive/logger'; import { PrismaStorage } from 'document-drive/storage/prisma'; @@ -13,11 +13,13 @@ import * as DocumentModelsLibs from 'document-model-libs/document-models'; import { module as DocumentModelLib } from 'document-model/document-model'; import { DocumentModel, Operation } from 'document-model/document'; import { - Listener, - ListenerFilter, - actions, - DocumentDriveAction + Listener, + ListenerFilter, + actions, + DocumentDriveAction } from 'document-model-libs/document-drive'; + +import * as sow from 'document-model-libs/scope-of-work'; import RedisCache from 'document-drive/cache/redis'; import MemoryCache from 'document-drive/cache/memory'; import { RedisQueueManager } from 'document-drive/queue/redis'; @@ -27,6 +29,7 @@ import { init } from './listenerManager'; import { getChildLogger } from '../../logger'; import DocumentDriveError from '../../errors/DocumentDriveError'; import { redisClient } from '../../redis'; +import { ScopeOfWorkAction, ScopeOfWorkDocument } from '../../../../../document-model-libs/dist/document-models/scope-of-work'; const logger = getChildLogger({ msgPrefix: 'Document Model' }); @@ -35,203 +38,230 @@ const documentDriveLogger = getChildLogger({ msgPrefix: "Document Drive" }); // patches the log method into the info method from pino const loggerAdapter = new Proxy(documentDriveLogger as unknown as ILogger, { - get: (target, prop) => - prop === "log" - ? documentDriveLogger.info - : target[prop as keyof ILogger], + get: (target, prop) => + prop === "log" + ? documentDriveLogger.info + : target[prop as keyof ILogger], }); setLogger(loggerAdapter); export function getDocumentDriveCRUD(prisma: Prisma.TransactionClient) { - const documentModels = [ - DocumentModelLib, - ...Object.values(DocumentModelsLibs), - ] as DocumentModel[]; - - - let driveServer: DocumentDriveServer; - - driveServer = new DocumentDriveServer( - documentModels, - new PrismaStorage(prisma as PrismaClient), - redisClient ? new RedisCache(redisClient) : new MemoryCache(), - redisClient ? new RedisQueueManager(3, 10, redisClient) : new BaseQueueManager(3, 10), - ); - - initialize(); - async function initialize() { - try { - await driveServer.initialize(); - await init(driveServer, prisma); - } catch (e: any) { - throw new DocumentDriveError({ code: 500, message: e.message ?? "Failed to initialize drive server", logging: true, context: e }) + const documentModels = [ + DocumentModelLib, + ...Object.values(DocumentModelsLibs), + ] as DocumentModel[]; + + + let driveServer: DocumentDriveServer; + + driveServer = new DocumentDriveServer( + documentModels, + new PrismaStorage(prisma as PrismaClient), + redisClient ? new RedisCache(redisClient) : new MemoryCache(), + redisClient ? new RedisQueueManager(3, 10, redisClient) : new BaseQueueManager(3, 10), + ); + + initialize(); + async function initialize() { + try { + await driveServer.initialize(); + await init(driveServer, prisma); + } catch (e: any) { + throw new DocumentDriveError({ code: 500, message: e.message ?? "Failed to initialize drive server", logging: true, context: e }) + } } - } - async function getTransmitter(driveId: string, transmitterId: string) { - const transmitter = await driveServer.getTransmitter(driveId, transmitterId) as PullResponderTransmitter; - if (!transmitter) { - throw new Error(`Transmitter ${transmitterId} not found`) + async function getTransmitter(driveId: string, transmitterId: string) { + const transmitter = await driveServer.getTransmitter(driveId, transmitterId) as PullResponderTransmitter; + if (!transmitter) { + throw new Error(`Transmitter ${transmitterId} not found`) + } + return transmitter } - return transmitter - } - - return { - addDrive: async (args: DriveInput) => { - try { - const drive = await driveServer!.addDrive(args); - await initialize(); - return drive; - } catch (e) { - logger.error(e); - throw new Error("Couldn't add drive"); - } - }, - deleteDrive: async (id: string) => { - try { - await driveServer.deleteDrive(id); - } catch (e) { - logger.error(e); - throw new Error("Couldn't delete drive"); - } - - return { id }; - }, - getDrive: async (id: string) => { - try { - const { state } = await driveServer.getDrive(id); - return state.global; - } catch (e) { - logger.error(e); - throw new Error("Couldn't get drive"); - } - }, - getDrives: async () => { - try { - const driveIds = await driveServer.getDrives() - return driveIds; - } catch (e) { - logger.error(e); - throw new Error("Couldn't get drives"); - } - }, - - pushUpdates: async ( - driveId: string, - operations: Operation[], - documentId?: string, - ) => { - if (!documentId) { - logger.info('adding drive operations') - const result = await driveServer.queueDriveOperations( - driveId, - operations, - ); - - return result; - } - logger.info('adding operations to document') - const result = await driveServer.queueOperations( - driveId, - documentId, - operations, - ); - return result; - }, - - pullStrands: async ( - driveId: string, - listenerId: string, - since?: string, - ): Promise => { - - const transmitter = await getTransmitter(driveId, listenerId); - if (transmitter.getStrands) { - const result = await transmitter.getStrands(since || undefined); - return result; - } - - return [] - }, - - processAcknowledge: async ( - driveId: string, - listenerId: string, - revisions: ListenerRevision[], - ) => { - const transmitter = await getTransmitter(driveId, listenerId); - const result = await transmitter.processAcknowledge( - driveId, - listenerId, - revisions, - ); - - return result; - }, - - registerPullResponderListener: async ( - driveId: string, - filter: ListenerFilter, - ): Promise => { - const uuid = generateUUID(); - const listener: Listener = { - block: false, - callInfo: { - data: '', - name: 'PullResponder', - transmitterType: 'PullResponder', + + return { + addDrive: async (args: DriveInput) => { + try { + const drive = await driveServer!.addDrive(args); + await initialize(); + return drive; + } catch (e) { + logger.error(e); + throw new Error("Couldn't add drive"); + } + }, + deleteDrive: async (id: string) => { + try { + await driveServer.deleteDrive(id); + } catch (e) { + logger.error(e); + throw new Error("Couldn't delete drive"); + } + + return { id }; + }, + getDrive: async (id: string) => { + try { + const { state } = await driveServer.getDrive(id); + return state.global; + } catch (e) { + logger.error(e); + throw new Error("Couldn't get drive"); + } + }, + getDrives: async () => { + try { + const driveIds = await driveServer.getDrives() + return driveIds; + } catch (e) { + logger.error(e); + throw new Error("Couldn't get drives"); + } }, - filter: { - branch: filter.branch ?? [], - documentId: filter.documentId ?? [], - documentType: filter.documentType ?? [], - scope: filter.scope ?? [], + + pushUpdates: async ( + driveId: string, + operations: Operation[], + documentId?: string, + ) => { + if (!documentId) { + logger.info('adding drive operations') + const result = await driveServer.queueDriveOperations( + driveId, + operations, + ); + + return result; + } + logger.info('adding operations to document') + const result = await driveServer.queueOperations( + driveId, + documentId, + operations, + ); + return result; + }, + + pullStrands: async ( + driveId: string, + listenerId: string, + since?: string, + ): Promise => { + + const transmitter = await getTransmitter(driveId, listenerId); + if (transmitter.getStrands) { + const result = await transmitter.getStrands(since || undefined); + return result; + } + + return [] + }, + + processAcknowledge: async ( + driveId: string, + listenerId: string, + revisions: ListenerRevision[], + ) => { + const transmitter = await getTransmitter(driveId, listenerId); + const result = await transmitter.processAcknowledge( + driveId, + listenerId, + revisions, + ); + + return result; }, - label: `Pullresponder #${uuid}`, - listenerId: uuid, - system: false, - }; - - const result = await driveServer.addDriveAction(driveId, actions.addListener({ listener })); - if (result.status !== "SUCCESS") { - result.error && logger.error(result.error); - throw new Error(`Listener couldn't be registered: ${result.error || result.status}`); - } - - return listener; - }, - - deletePullResponderListener: async ( - driveId: string, - listenerId: string, - ) => { - const result = await driveServer.addDriveAction(driveId, actions.removeListener({ listenerId })); - if (result.status !== "SUCCESS") { - result.error && logger.error(result.error); - throw new Error(`Listener couldn't be deleted: ${result.error || result.status}`); - } - - return listenerId; - }, - - getDocument: async ( - driveId: string, - documentId: string, - ) => { - const document = await driveServer.getDocument(driveId, documentId); - const response = { - ...document, - id: documentId, - revision: document.revision.global, - state: document.state.global, - operations: document.operations.global, - }; - return response; - }, - - getDocuments: async (driveId: string) => { - const documents = await driveServer.getDocuments(driveId); - return documents; + + registerPullResponderListener: async ( + driveId: string, + filter: ListenerFilter, + ): Promise => { + const uuid = generateUUID(); + const listener: Listener = { + block: false, + callInfo: { + data: '', + name: 'PullResponder', + transmitterType: 'PullResponder', + }, + filter: { + branch: filter.branch ?? [], + documentId: filter.documentId ?? [], + documentType: filter.documentType ?? [], + scope: filter.scope ?? [], + }, + label: `Pullresponder #${uuid}`, + listenerId: uuid, + system: false, + }; + + const result = await driveServer.addDriveAction(driveId, actions.addListener({ listener })); + if (result.status !== "SUCCESS") { + result.error && logger.error(result.error); + throw new Error(`Listener couldn't be registered: ${result.error || result.status}`); + } + + return listener; + }, + + deletePullResponderListener: async ( + driveId: string, + listenerId: string, + ) => { + const result = await driveServer.addDriveAction(driveId, actions.removeListener({ listenerId })); + if (result.status !== "SUCCESS") { + result.error && logger.error(result.error); + throw new Error(`Listener couldn't be deleted: ${result.error || result.status}`); + } + + return listenerId; + }, + + getDocument: async ( + driveId: string, + documentId: string, + ) => { + const document = await driveServer.getDocument(driveId, documentId); + const response = { + ...document, + id: documentId, + revision: document.revision.global, + state: document.state.global, + operations: document.operations.global, + }; + return response; + }, + + getDocuments: async (driveId: string) => { + const documents = await driveServer.getDocuments(driveId); + return documents; + }, + + closeScopeOfWorkIssue: async (githubId: string) => { + const dbEntry = await prisma.scopeOfWorkDeliverable.findFirst({ + where: { + githubId: githubId + } + }) + + if (!dbEntry) { + throw new Error("Deliverable not found"); + } + + const { driveId, documentId, id } = dbEntry; + + const sowDocument = await driveServer.getDocument(driveId, documentId) as ScopeOfWorkDocument; + if (!sowDocument) { + throw new Error("Document not found"); + } + + const result = await driveServer.addAction(driveId, documentId, sow.actions.updateDeliverableStatus({ + id, + status: "DELIVERED" + })) + + + return result; + } } - } } diff --git a/api/src/modules/scope-of-work/listener.ts b/api/src/modules/scope-of-work/listener.ts index 1a453b66..af6402c8 100644 --- a/api/src/modules/scope-of-work/listener.ts +++ b/api/src/modules/scope-of-work/listener.ts @@ -1,5 +1,5 @@ import { InternalTransmitterUpdate } from "document-drive"; -import {ScopeOfWorkDocument, CreateDeliverableInput } from "document-model-libs/scope-of-work"; +import { ScopeOfWorkDocument, CreateDeliverableInput } from "document-model-libs/scope-of-work"; import { getChildLogger } from "../../logger"; import { Prisma } from "@prisma/client"; import { Octokit } from "@octokit/rest"; @@ -8,7 +8,7 @@ const GITHUB_REPO_OWNER = "powerhouse-inc"; const GITHUB_REPO_NAME = "powerhouse-mirror"; const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN, - }); +}); const logger = getChildLogger({ msgPrefix: 'RWA Internal Listener' }, { moduleName: "RWA Internal Listener" }); export const options: any = { @@ -38,8 +38,8 @@ export async function transmit(strands: InternalTransmitterUpdate, prisma: Prisma.TransactionClient) { - for(let op of strand.operations) { - if(op.type === "CREATE_DELIVERABLE") { + for (let op of strand.operations) { + if (op.type === "CREATE_DELIVERABLE") { const input = op.input as CreateDeliverableInput; await updateDeliverableInDb(strand.driveId, strand.documentId, input.id, prisma) } @@ -55,13 +55,15 @@ async function updateDeliverableInDb(driveId: string, documentId: string, delive documentId: documentId } }) + + // already exists in db return true; } catch (e) { logger.info("deliverable not found in db") } try { - + const result = await createGitHubIssue("New Deliverable Created", "A new deliverable has been created in the scope of work document") await prisma.scopeOfWorkDeliverable.create({ data: { id: deliverableId, @@ -70,11 +72,10 @@ async function updateDeliverableInDb(driveId: string, documentId: string, delive title: "Deliverable", description: "Description", status: "NOT_STARTED", - githubCreated: true + githubCreated: true, + githubId: result.data.number } }) - - await createGitHubIssue("New Deliverable Created", "A new deliverable has been created in the scope of work document") } catch (e) { console.log("Error creating github issue") } @@ -82,14 +83,16 @@ async function updateDeliverableInDb(driveId: string, documentId: string, delive async function createGitHubIssue(title: string, body: string) { try { - await octokit.issues.create({ + const result = await octokit.issues.create({ owner: GITHUB_REPO_OWNER, repo: GITHUB_REPO_NAME, title: title, body: body, }); + + return result; } catch (error) { - logger.error({msg: "Error creating GitHub issue:", error}); - throw error; + logger.error({ msg: "Error creating GitHub issue:", error }); + throw error; } }