diff --git a/services/libs/data-access-layer/src/evaluated-projects/evaluatedProjects.ts b/services/libs/data-access-layer/src/evaluated-projects/evaluatedProjects.ts new file mode 100644 index 0000000000..caec6a72a1 --- /dev/null +++ b/services/libs/data-access-layer/src/evaluated-projects/evaluatedProjects.ts @@ -0,0 +1,397 @@ +import { QueryExecutor } from '../queryExecutor' +import { prepareSelectColumns } from '../utils' + +import { + EvaluationStatus, + IDbEvaluatedProject, + IDbEvaluatedProjectCreate, + IDbEvaluatedProjectUpdate, +} from './types' + +const EVALUATED_PROJECT_COLUMNS = [ + 'id', + 'projectCatalogId', + 'evaluationStatus', + 'evaluationScore', + 'evaluation', + 'evaluationReason', + 'evaluatedAt', + 'starsCount', + 'forksCount', + 'commitsCount', + 'pullRequestsCount', + 'issuesCount', + 'onboarded', + 'onboardedAt', + 'createdAt', + 'updatedAt', +] + +export async function findEvaluatedProjectById( + qx: QueryExecutor, + id: string, +): Promise { + return qx.selectOneOrNone( + ` + SELECT ${prepareSelectColumns(EVALUATED_PROJECT_COLUMNS)} + FROM "evaluatedProjects" + WHERE id = $(id) + `, + { id }, + ) +} + +export async function findEvaluatedProjectByProjectCatalogId( + qx: QueryExecutor, + projectCatalogId: string, +): Promise { + return qx.selectOneOrNone( + ` + SELECT ${prepareSelectColumns(EVALUATED_PROJECT_COLUMNS)} + FROM "evaluatedProjects" + WHERE "projectCatalogId" = $(projectCatalogId) + `, + { projectCatalogId }, + ) +} + +export async function findEvaluatedProjectsByStatus( + qx: QueryExecutor, + evaluationStatus: EvaluationStatus, + options: { limit?: number; offset?: number } = {}, +): Promise { + const { limit, offset } = options + + return qx.select( + ` + SELECT ${prepareSelectColumns(EVALUATED_PROJECT_COLUMNS)} + FROM "evaluatedProjects" + WHERE "evaluationStatus" = $(evaluationStatus) + ORDER BY "createdAt" ASC + ${limit !== undefined ? 'LIMIT $(limit)' : ''} + ${offset !== undefined ? 'OFFSET $(offset)' : ''} + `, + { evaluationStatus, limit, offset }, + ) +} + +export async function findAllEvaluatedProjects( + qx: QueryExecutor, + options: { limit?: number; offset?: number } = {}, +): Promise { + const { limit, offset } = options + + return qx.select( + ` + SELECT ${prepareSelectColumns(EVALUATED_PROJECT_COLUMNS)} + FROM "evaluatedProjects" + ORDER BY "createdAt" DESC + ${limit !== undefined ? 'LIMIT $(limit)' : ''} + ${offset !== undefined ? 'OFFSET $(offset)' : ''} + `, + { limit, offset }, + ) +} + +export async function countEvaluatedProjects( + qx: QueryExecutor, + evaluationStatus?: EvaluationStatus, +): Promise { + const statusFilter = evaluationStatus ? 'WHERE "evaluationStatus" = $(evaluationStatus)' : '' + + const result = await qx.selectOne( + ` + SELECT COUNT(*) AS count + FROM "evaluatedProjects" + ${statusFilter} + `, + { evaluationStatus }, + ) + return parseInt(result.count, 10) +} + +export async function insertEvaluatedProject( + qx: QueryExecutor, + data: IDbEvaluatedProjectCreate, +): Promise { + return qx.selectOne( + ` + INSERT INTO "evaluatedProjects" ( + "projectCatalogId", + "evaluationStatus", + "evaluationScore", + evaluation, + "evaluationReason", + "starsCount", + "forksCount", + "commitsCount", + "pullRequestsCount", + "issuesCount", + "createdAt", + "updatedAt" + ) + VALUES ( + $(projectCatalogId), + $(evaluationStatus), + $(evaluationScore), + $(evaluation), + $(evaluationReason), + $(starsCount), + $(forksCount), + $(commitsCount), + $(pullRequestsCount), + $(issuesCount), + NOW(), + NOW() + ) + RETURNING ${prepareSelectColumns(EVALUATED_PROJECT_COLUMNS)} + `, + { + projectCatalogId: data.projectCatalogId, + evaluationStatus: data.evaluationStatus ?? 'pending', + evaluationScore: data.evaluationScore ?? null, + evaluation: data.evaluation ? JSON.stringify(data.evaluation) : null, + evaluationReason: data.evaluationReason ?? null, + starsCount: data.starsCount ?? null, + forksCount: data.forksCount ?? null, + commitsCount: data.commitsCount ?? null, + pullRequestsCount: data.pullRequestsCount ?? null, + issuesCount: data.issuesCount ?? null, + }, + ) +} + +export async function bulkInsertEvaluatedProjects( + qx: QueryExecutor, + items: IDbEvaluatedProjectCreate[], +): Promise { + if (items.length === 0) { + return + } + + const values = items.map((item) => ({ + projectCatalogId: item.projectCatalogId, + evaluationStatus: item.evaluationStatus ?? 'pending', + evaluationScore: item.evaluationScore ?? null, + evaluation: item.evaluation ? JSON.stringify(item.evaluation) : null, + evaluationReason: item.evaluationReason ?? null, + starsCount: item.starsCount ?? null, + forksCount: item.forksCount ?? null, + commitsCount: item.commitsCount ?? null, + pullRequestsCount: item.pullRequestsCount ?? null, + issuesCount: item.issuesCount ?? null, + })) + + await qx.result( + ` + INSERT INTO "evaluatedProjects" ( + "projectCatalogId", + "evaluationStatus", + "evaluationScore", + evaluation, + "evaluationReason", + "starsCount", + "forksCount", + "commitsCount", + "pullRequestsCount", + "issuesCount", + "createdAt", + "updatedAt" + ) + SELECT + v."projectCatalogId"::uuid, + v."evaluationStatus", + v."evaluationScore"::double precision, + v.evaluation::jsonb, + v."evaluationReason", + v."starsCount"::integer, + v."forksCount"::integer, + v."commitsCount"::integer, + v."pullRequestsCount"::integer, + v."issuesCount"::integer, + NOW(), + NOW() + FROM jsonb_to_recordset($(values)::jsonb) AS v( + "projectCatalogId" text, + "evaluationStatus" text, + "evaluationScore" double precision, + evaluation jsonb, + "evaluationReason" text, + "starsCount" integer, + "forksCount" integer, + "commitsCount" integer, + "pullRequestsCount" integer, + "issuesCount" integer + ) + `, + { values: JSON.stringify(values) }, + ) +} + +export async function updateEvaluatedProject( + qx: QueryExecutor, + id: string, + data: IDbEvaluatedProjectUpdate, +): Promise { + const setClauses: string[] = [] + const params: Record = { id } + + if (data.evaluationStatus !== undefined) { + setClauses.push('"evaluationStatus" = $(evaluationStatus)') + params.evaluationStatus = data.evaluationStatus + } + if (data.evaluationScore !== undefined) { + setClauses.push('"evaluationScore" = $(evaluationScore)') + params.evaluationScore = data.evaluationScore + } + if (data.evaluation !== undefined) { + setClauses.push('evaluation = $(evaluation)') + params.evaluation = data.evaluation ? JSON.stringify(data.evaluation) : null + } + if (data.evaluationReason !== undefined) { + setClauses.push('"evaluationReason" = $(evaluationReason)') + params.evaluationReason = data.evaluationReason + } + if (data.evaluatedAt !== undefined) { + setClauses.push('"evaluatedAt" = $(evaluatedAt)') + params.evaluatedAt = data.evaluatedAt + } + if (data.starsCount !== undefined) { + setClauses.push('"starsCount" = $(starsCount)') + params.starsCount = data.starsCount + } + if (data.forksCount !== undefined) { + setClauses.push('"forksCount" = $(forksCount)') + params.forksCount = data.forksCount + } + if (data.commitsCount !== undefined) { + setClauses.push('"commitsCount" = $(commitsCount)') + params.commitsCount = data.commitsCount + } + if (data.pullRequestsCount !== undefined) { + setClauses.push('"pullRequestsCount" = $(pullRequestsCount)') + params.pullRequestsCount = data.pullRequestsCount + } + if (data.issuesCount !== undefined) { + setClauses.push('"issuesCount" = $(issuesCount)') + params.issuesCount = data.issuesCount + } + if (data.onboarded !== undefined) { + setClauses.push('onboarded = $(onboarded)') + params.onboarded = data.onboarded + } + if (data.onboardedAt !== undefined) { + setClauses.push('"onboardedAt" = $(onboardedAt)') + params.onboardedAt = data.onboardedAt + } + + if (setClauses.length === 0) { + return findEvaluatedProjectById(qx, id) + } + + return qx.selectOneOrNone( + ` + UPDATE "evaluatedProjects" + SET + ${setClauses.join(',\n ')}, + "updatedAt" = NOW() + WHERE id = $(id) + RETURNING ${prepareSelectColumns(EVALUATED_PROJECT_COLUMNS)} + `, + params, + ) +} + +export async function markEvaluatedProjectAsEvaluated( + qx: QueryExecutor, + id: string, + data: { + evaluationScore: number + evaluation: Record + evaluationReason?: string + }, +): Promise { + return qx.selectOneOrNone( + ` + UPDATE "evaluatedProjects" + SET + "evaluationStatus" = 'evaluated', + "evaluationScore" = $(evaluationScore), + evaluation = $(evaluation), + "evaluationReason" = $(evaluationReason), + "evaluatedAt" = NOW(), + "updatedAt" = NOW() + WHERE id = $(id) + RETURNING ${prepareSelectColumns(EVALUATED_PROJECT_COLUMNS)} + `, + { + id, + evaluationScore: data.evaluationScore, + evaluation: JSON.stringify(data.evaluation), + evaluationReason: data.evaluationReason ?? null, + }, + ) +} + +export async function markEvaluatedProjectAsOnboarded( + qx: QueryExecutor, + id: string, +): Promise { + await qx.selectNone( + ` + UPDATE "evaluatedProjects" + SET + onboarded = true, + "onboardedAt" = NOW(), + "updatedAt" = NOW() + WHERE id = $(id) + `, + { id }, + ) +} + +export async function deleteEvaluatedProject(qx: QueryExecutor, id: string): Promise { + return qx.result( + ` + DELETE FROM "evaluatedProjects" + WHERE id = $(id) + `, + { id }, + ) +} + +export async function deleteEvaluatedProjectByProjectCatalogId( + qx: QueryExecutor, + projectCatalogId: string, +): Promise { + return qx.result( + ` + DELETE FROM "evaluatedProjects" + WHERE "projectCatalogId" = $(projectCatalogId) + `, + { projectCatalogId }, + ) +} + +export async function findPendingEvaluatedProjectsWithCatalog( + qx: QueryExecutor, + options: { limit?: number } = {}, +): Promise<(IDbEvaluatedProject & { projectSlug: string; repoName: string; repoUrl: string })[]> { + const { limit } = options + + return qx.select( + ` + SELECT + ${prepareSelectColumns(EVALUATED_PROJECT_COLUMNS, 'ep')}, + pc."projectSlug", + pc."repoName", + pc."repoUrl" + FROM "evaluatedProjects" ep + JOIN "projectCatalog" pc ON pc.id = ep."projectCatalogId" + WHERE ep."evaluationStatus" = 'pending' + ORDER BY ep."createdAt" ASC + ${limit !== undefined ? 'LIMIT $(limit)' : ''} + `, + { limit }, + ) +} diff --git a/services/libs/data-access-layer/src/evaluated-projects/index.ts b/services/libs/data-access-layer/src/evaluated-projects/index.ts new file mode 100644 index 0000000000..7a4064eec2 --- /dev/null +++ b/services/libs/data-access-layer/src/evaluated-projects/index.ts @@ -0,0 +1,2 @@ +export * from './types' +export * from './evaluatedProjects' diff --git a/services/libs/data-access-layer/src/evaluated-projects/types.ts b/services/libs/data-access-layer/src/evaluated-projects/types.ts new file mode 100644 index 0000000000..f8661f47a2 --- /dev/null +++ b/services/libs/data-access-layer/src/evaluated-projects/types.ts @@ -0,0 +1,69 @@ +export type EvaluationStatus = 'pending' | 'evaluating' | 'evaluated' | 'failed' + +export interface IDbEvaluatedProject { + id: string + projectCatalogId: string + evaluationStatus: EvaluationStatus + evaluationScore: number | null + evaluation: Record | null + evaluationReason: string | null + evaluatedAt: string | null + starsCount: number | null + forksCount: number | null + commitsCount: number | null + pullRequestsCount: number | null + issuesCount: number | null + onboarded: boolean + onboardedAt: string | null + createdAt: string | null + updatedAt: string | null +} + +type EvaluatedProjectWritable = Pick< + IDbEvaluatedProject, + | 'projectCatalogId' + | 'evaluationStatus' + | 'evaluationScore' + | 'evaluation' + | 'evaluationReason' + | 'evaluatedAt' + | 'starsCount' + | 'forksCount' + | 'commitsCount' + | 'pullRequestsCount' + | 'issuesCount' + | 'onboarded' + | 'onboardedAt' +> + +export type IDbEvaluatedProjectCreate = Omit & { + projectCatalogId: string +} & { + evaluationStatus?: EvaluationStatus + evaluationScore?: number + evaluation?: Record + evaluationReason?: string + evaluatedAt?: string + starsCount?: number + forksCount?: number + commitsCount?: number + pullRequestsCount?: number + issuesCount?: number + onboarded?: boolean + onboardedAt?: string +} + +export type IDbEvaluatedProjectUpdate = Partial<{ + evaluationStatus: EvaluationStatus + evaluationScore: number + evaluation: Record + evaluationReason: string + evaluatedAt: string + starsCount: number + forksCount: number + commitsCount: number + pullRequestsCount: number + issuesCount: number + onboarded: boolean + onboardedAt: string +}> diff --git a/services/libs/data-access-layer/src/index.ts b/services/libs/data-access-layer/src/index.ts index 6be1f0fe22..fdd46be807 100644 --- a/services/libs/data-access-layer/src/index.ts +++ b/services/libs/data-access-layer/src/index.ts @@ -9,3 +9,5 @@ export * from './repositories' export * from './security_insights' export * from './systemSettings' export * from './integrations' +export * from './project-catalog' +export * from './evaluated-projects' diff --git a/services/libs/data-access-layer/src/project-catalog/index.ts b/services/libs/data-access-layer/src/project-catalog/index.ts new file mode 100644 index 0000000000..af7ef7faa1 --- /dev/null +++ b/services/libs/data-access-layer/src/project-catalog/index.ts @@ -0,0 +1,2 @@ +export * from './types' +export * from './projectCatalog' diff --git a/services/libs/data-access-layer/src/project-catalog/projectCatalog.ts b/services/libs/data-access-layer/src/project-catalog/projectCatalog.ts new file mode 100644 index 0000000000..2e3b409579 --- /dev/null +++ b/services/libs/data-access-layer/src/project-catalog/projectCatalog.ts @@ -0,0 +1,315 @@ +import { QueryExecutor } from '../queryExecutor' +import { prepareSelectColumns } from '../utils' + +import { IDbProjectCatalog, IDbProjectCatalogCreate, IDbProjectCatalogUpdate } from './types' + +const PROJECT_CATALOG_COLUMNS = [ + 'id', + 'projectSlug', + 'repoName', + 'repoUrl', + 'criticalityScore', + 'syncedAt', + 'createdAt', + 'updatedAt', +] + +export async function findProjectCatalogById( + qx: QueryExecutor, + id: string, +): Promise { + return qx.selectOneOrNone( + ` + SELECT ${prepareSelectColumns(PROJECT_CATALOG_COLUMNS)} + FROM "projectCatalog" + WHERE id = $(id) + `, + { id }, + ) +} + +export async function findProjectCatalogByRepoUrl( + qx: QueryExecutor, + repoUrl: string, +): Promise { + return qx.selectOneOrNone( + ` + SELECT ${prepareSelectColumns(PROJECT_CATALOG_COLUMNS)} + FROM "projectCatalog" + WHERE "repoUrl" = $(repoUrl) + `, + { repoUrl }, + ) +} + +export async function findProjectCatalogBySlug( + qx: QueryExecutor, + projectSlug: string, +): Promise { + return qx.select( + ` + SELECT ${prepareSelectColumns(PROJECT_CATALOG_COLUMNS)} + FROM "projectCatalog" + WHERE "projectSlug" = $(projectSlug) + ORDER BY "createdAt" DESC + `, + { projectSlug }, + ) +} + +export async function findAllProjectCatalog( + qx: QueryExecutor, + options: { limit?: number; offset?: number } = {}, +): Promise { + const { limit, offset } = options + + return qx.select( + ` + SELECT ${prepareSelectColumns(PROJECT_CATALOG_COLUMNS)} + FROM "projectCatalog" + ORDER BY "createdAt" DESC + ${limit !== undefined ? 'LIMIT $(limit)' : ''} + ${offset !== undefined ? 'OFFSET $(offset)' : ''} + `, + { limit, offset }, + ) +} + +export async function countProjectCatalog(qx: QueryExecutor): Promise { + const result = await qx.selectOne( + ` + SELECT COUNT(*) AS count + FROM "projectCatalog" + `, + ) + return parseInt(result.count, 10) +} + +export async function insertProjectCatalog( + qx: QueryExecutor, + data: IDbProjectCatalogCreate, +): Promise { + return qx.selectOne( + ` + INSERT INTO "projectCatalog" ( + "projectSlug", + "repoName", + "repoUrl", + "criticalityScore", + "createdAt", + "updatedAt" + ) + VALUES ( + $(projectSlug), + $(repoName), + $(repoUrl), + $(criticalityScore), + NOW(), + NOW() + ) + RETURNING ${prepareSelectColumns(PROJECT_CATALOG_COLUMNS)} + `, + { + projectSlug: data.projectSlug, + repoName: data.repoName, + repoUrl: data.repoUrl, + criticalityScore: data.criticalityScore ?? null, + }, + ) +} + +export async function bulkInsertProjectCatalog( + qx: QueryExecutor, + items: IDbProjectCatalogCreate[], +): Promise { + if (items.length === 0) { + return + } + + const values = items.map((item) => ({ + projectSlug: item.projectSlug, + repoName: item.repoName, + repoUrl: item.repoUrl, + criticalityScore: item.criticalityScore ?? null, + })) + + await qx.result( + ` + INSERT INTO "projectCatalog" ( + "projectSlug", + "repoName", + "repoUrl", + "criticalityScore", + "createdAt", + "updatedAt" + ) + SELECT + v."projectSlug", + v."repoName", + v."repoUrl", + v."criticalityScore"::double precision, + NOW(), + NOW() + FROM jsonb_to_recordset($(values)::jsonb) AS v( + "projectSlug" text, + "repoName" text, + "repoUrl" text, + "criticalityScore" double precision + ) + `, + { values: JSON.stringify(values) }, + ) +} + +export async function upsertProjectCatalog( + qx: QueryExecutor, + data: IDbProjectCatalogCreate, +): Promise { + return qx.selectOne( + ` + INSERT INTO "projectCatalog" ( + "projectSlug", + "repoName", + "repoUrl", + "criticalityScore", + "createdAt", + "updatedAt" + ) + VALUES ( + $(projectSlug), + $(repoName), + $(repoUrl), + $(criticalityScore), + NOW(), + NOW() + ) + ON CONFLICT ("repoUrl") DO UPDATE SET + "projectSlug" = EXCLUDED."projectSlug", + "repoName" = EXCLUDED."repoName", + "criticalityScore" = EXCLUDED."criticalityScore", + "updatedAt" = NOW() + RETURNING ${prepareSelectColumns(PROJECT_CATALOG_COLUMNS)} + `, + { + projectSlug: data.projectSlug, + repoName: data.repoName, + repoUrl: data.repoUrl, + criticalityScore: data.criticalityScore ?? null, + }, + ) +} + +export async function bulkUpsertProjectCatalog( + qx: QueryExecutor, + items: IDbProjectCatalogCreate[], +): Promise { + if (items.length === 0) { + return + } + + const values = items.map((item) => ({ + projectSlug: item.projectSlug, + repoName: item.repoName, + repoUrl: item.repoUrl, + criticalityScore: item.criticalityScore ?? null, + })) + + await qx.result( + ` + INSERT INTO "projectCatalog" ( + "projectSlug", + "repoName", + "repoUrl", + "criticalityScore", + "createdAt", + "updatedAt" + ) + SELECT + v."projectSlug", + v."repoName", + v."repoUrl", + v."criticalityScore"::double precision, + NOW(), + NOW() + FROM jsonb_to_recordset($(values)::jsonb) AS v( + "projectSlug" text, + "repoName" text, + "repoUrl" text, + "criticalityScore" double precision + ) + ON CONFLICT ("repoUrl") DO UPDATE SET + "projectSlug" = EXCLUDED."projectSlug", + "repoName" = EXCLUDED."repoName", + "criticalityScore" = EXCLUDED."criticalityScore", + "updatedAt" = NOW() + `, + { values: JSON.stringify(values) }, + ) +} + +export async function updateProjectCatalog( + qx: QueryExecutor, + id: string, + data: IDbProjectCatalogUpdate, +): Promise { + const setClauses: string[] = [] + const params: Record = { id } + + if (data.projectSlug !== undefined) { + setClauses.push('"projectSlug" = $(projectSlug)') + params.projectSlug = data.projectSlug + } + if (data.repoName !== undefined) { + setClauses.push('"repoName" = $(repoName)') + params.repoName = data.repoName + } + if (data.repoUrl !== undefined) { + setClauses.push('"repoUrl" = $(repoUrl)') + params.repoUrl = data.repoUrl + } + if (data.criticalityScore !== undefined) { + setClauses.push('"criticalityScore" = $(criticalityScore)') + params.criticalityScore = data.criticalityScore + } + if (data.syncedAt !== undefined) { + setClauses.push('"syncedAt" = $(syncedAt)') + params.syncedAt = data.syncedAt + } + + if (setClauses.length === 0) { + return findProjectCatalogById(qx, id) + } + + return qx.selectOneOrNone( + ` + UPDATE "projectCatalog" + SET + ${setClauses.join(',\n ')}, + "updatedAt" = NOW() + WHERE id = $(id) + RETURNING ${prepareSelectColumns(PROJECT_CATALOG_COLUMNS)} + `, + params, + ) +} + +export async function updateProjectCatalogSyncedAt(qx: QueryExecutor, id: string): Promise { + await qx.selectNone( + ` + UPDATE "projectCatalog" + SET "syncedAt" = NOW(), "updatedAt" = NOW() + WHERE id = $(id) + `, + { id }, + ) +} + +export async function deleteProjectCatalog(qx: QueryExecutor, id: string): Promise { + return qx.result( + ` + DELETE FROM "projectCatalog" + WHERE id = $(id) + `, + { id }, + ) +} diff --git a/services/libs/data-access-layer/src/project-catalog/types.ts b/services/libs/data-access-layer/src/project-catalog/types.ts new file mode 100644 index 0000000000..382527f57f --- /dev/null +++ b/services/libs/data-access-layer/src/project-catalog/types.ts @@ -0,0 +1,23 @@ +export interface IDbProjectCatalog { + id: string + projectSlug: string + repoName: string + repoUrl: string + criticalityScore: number | null + syncedAt: string | null + createdAt: string | null + updatedAt: string | null +} + +type ProjectCatalogWritable = Pick< + IDbProjectCatalog, + 'projectSlug' | 'repoName' | 'repoUrl' | 'criticalityScore' +> + +export type IDbProjectCatalogCreate = Omit & { + criticalityScore?: number +} + +export type IDbProjectCatalogUpdate = Partial & { + syncedAt?: string +}