diff --git a/__fixtures__/index.js b/__fixtures__/index.js
index 768a3ef..810062e 100644
--- a/__fixtures__/index.js
+++ b/__fixtures__/index.js
@@ -920,10 +920,121 @@ const sampleBulkImportFileContent = [{
is_subscribed: true
}]
+// Sample project data for testing validateProjectData
+const sampleProjectData = {
+ project: {
+ id: 1,
+ name: 'Test Project',
+ description: 'A test project',
+ created_at: '2023-01-01T00:00:00Z',
+ updated_at: '2023-01-02T00:00:00Z'
+ },
+ checks: [
+ {
+ id: 1,
+ checklist_id: 1,
+ description: 'Test check',
+ created_at: '2023-01-01T00:00:00Z',
+ updated_at: '2023-01-02T00:00:00Z'
+ }
+ ],
+ results: [
+ {
+ id: 1,
+ project_id: 1,
+ name: 'Test result',
+ score: 8.5,
+ created_at: '2023-01-01T00:00:00Z',
+ updated_at: '2023-01-02T00:00:00Z'
+ }
+ ],
+ tasks: [
+ {
+ id: 1,
+ project_id: 1,
+ description: 'Test task',
+ implementation_status: 'pending',
+ created_at: '2023-01-01T00:00:00Z',
+ updated_at: '2023-01-02T00:00:00Z'
+ }
+ ],
+ alerts: [
+ {
+ id: 1,
+ project_id: 1,
+ title: 'Test alert',
+ description: 'This is a test alert',
+ severity: 'medium',
+ created_at: '2023-01-01T00:00:00Z',
+ updated_at: '2023-01-02T00:00:00Z'
+ }
+ ],
+ githubOrgs: [
+ {
+ id: 1,
+ login: 'test-org',
+ name: 'Test Organization',
+ created_at: '2023-01-01T00:00:00Z',
+ updated_at: '2023-01-02T00:00:00Z'
+ }
+ ],
+ githubRepos: [
+ {
+ id: 1,
+ name: 'test-repo',
+ full_name: 'test-org/test-repo',
+ created_at: '2023-01-01T00:00:00Z',
+ updated_at: '2023-01-02T00:00:00Z'
+ }
+ ],
+ ossfScorecardResults: [
+ {
+ id: 1,
+ repository_id: 1,
+ score: 8.5,
+ created_at: '2023-01-01T00:00:00Z',
+ updated_at: '2023-01-02T00:00:00Z'
+ }
+ ]
+}
+
+// Sample index data for testing validateIndexData
+const sampleIndexData = {
+ projects: [
+ {
+ id: 1,
+ name: 'Test Project',
+ description: 'A test project',
+ created_at: '2023-01-01T00:00:00Z',
+ updated_at: '2023-01-02T00:00:00Z'
+ }
+ ],
+ checklists: [
+ {
+ id: 1,
+ name: 'Test Checklist',
+ type: 'security',
+ created_at: '2023-01-01T00:00:00Z',
+ updated_at: '2023-01-02T00:00:00Z'
+ }
+ ],
+ checks: [
+ {
+ id: 1,
+ checklist_id: 1,
+ description: 'Test check',
+ created_at: '2023-01-01T00:00:00Z',
+ updated_at: '2023-01-02T00:00:00Z'
+ }
+ ]
+}
+
module.exports = {
sampleGithubOrg,
sampleGithubListOrgRepos,
sampleGithubRepository,
sampleOSSFScorecardResult,
- sampleBulkImportFileContent
+ sampleBulkImportFileContent,
+ sampleProjectData,
+ sampleIndexData
}
diff --git a/__tests__/httpServer.test.js b/__tests__/httpServer/apiV1.test.js
similarity index 93%
rename from __tests__/httpServer.test.js
rename to __tests__/httpServer/apiV1.test.js
index 652058f..3cbeb55 100644
--- a/__tests__/httpServer.test.js
+++ b/__tests__/httpServer/apiV1.test.js
@@ -1,12 +1,12 @@
const request = require('supertest')
-const { generateStaticReports } = require('../src/reports')
-const serverModule = require('../src/httpServer')
+const { generateStaticReports } = require('../../src/reports')
+const serverModule = require('../../src/httpServer')
let server
let serverStop
let app
// Mocks
-jest.mock('../src/reports', () => ({
+jest.mock('../../src/reports', () => ({
generateStaticReports: jest.fn()
}))
@@ -27,7 +27,7 @@ beforeEach(() => {
jest.clearAllMocks()
})
-describe('HTTP Server API', () => {
+describe('HTTP Server API V1', () => {
describe('GET /api/v1/__health', () => {
test('should return status ok', async () => {
const response = await app.get('/api/v1/__health')
diff --git a/__tests__/httpServer/website.test.js b/__tests__/httpServer/website.test.js
new file mode 100644
index 0000000..fd8a401
--- /dev/null
+++ b/__tests__/httpServer/website.test.js
@@ -0,0 +1,69 @@
+const request = require('supertest')
+const knexInit = require('knex')
+const serverModule = require('../../src/httpServer')
+const { getConfig } = require('../../src/config')
+const { resetDatabase, initializeStore } = require('../../__utils__')
+
+const { dbSettings } = getConfig('test')
+
+let server
+let serverStop
+let app
+let knex
+let addProject
+let testProjectId
+
+beforeAll(async () => {
+ // Initialize database
+ knex = knexInit(dbSettings);
+ ({ addProject } = initializeStore(knex))
+
+ // Reset database and add test project
+ await resetDatabase(knex)
+ const testProject = await addProject({ name: 'Test Project' })
+ testProjectId = testProject.id
+
+ // Initialize server asynchronously
+ const serverInstance = serverModule()
+ server = await serverInstance.start()
+ serverStop = serverInstance.stop
+ app = request(server)
+})
+
+afterAll(async () => {
+ // Cleanup after all tests
+ await serverStop?.()
+ await resetDatabase(knex)
+ await knex.destroy()
+})
+
+describe('HTTP Server WEBSITE', () => {
+ describe('GET /', () => {
+ it('should render index page', async () => {
+ const response = await app.get('/')
+ expect(response.status).toBe(200)
+ expect(response.header['content-type']).toMatch(/text\/html/)
+ })
+ })
+
+ describe('GET /projects/:id', () => {
+ it('should render project page for valid project ID', async () => {
+ const response = await app.get(`/projects/${testProjectId}`)
+ expect(response.status).toBe(200)
+ expect(response.header['content-type']).toMatch(/text\/html/)
+ })
+
+ it('should render notFound page for invalid project ID format', async () => {
+ const response = await app.get('/projects/invalid')
+ expect(response.status).toBe(404)
+ expect(response.header['content-type']).toMatch(/text\/html/)
+ })
+
+ it('should render notFound page for non-existent project ID', async () => {
+ const nonExistentId = 9999
+ const response = await app.get(`/projects/${nonExistentId}`)
+ expect(response.status).toBe(404)
+ expect(response.header['content-type']).toMatch(/text\/html/)
+ })
+ })
+})
diff --git a/__tests__/reports.test.js b/__tests__/reports.test.js
new file mode 100644
index 0000000..6746fcf
--- /dev/null
+++ b/__tests__/reports.test.js
@@ -0,0 +1,47 @@
+const { internalLinkBuilder } = require('../src/reports')
+
+describe('internalLinkBuilder', () => {
+ describe('static mode', () => {
+ const staticLinkBuilder = internalLinkBuilder('static')
+
+ test('should handle empty reference', () => {
+ expect(staticLinkBuilder('', null)).toBe('index.html')
+ })
+
+ test('should remove leading slash in static mode', () => {
+ expect(staticLinkBuilder('/assets/favicon.ico', null)).toBe('assets/favicon.ico')
+ })
+
+ test('should handle project references in static mode', () => {
+ const project = { name: 'testproject', id: 123 }
+ expect(staticLinkBuilder('', project)).toBe('testproject.html')
+ })
+
+ test('should prioritize project reference over path', () => {
+ const project = { name: 'testproject', id: 123 }
+ expect(staticLinkBuilder('/some/path', project)).toBe('testproject.html')
+ })
+ })
+
+ describe('server mode', () => {
+ const serverLinkBuilder = internalLinkBuilder('server')
+
+ test('should handle empty reference', () => {
+ expect(serverLinkBuilder('', null)).toBe('index.html')
+ })
+
+ test('should preserve leading slash in server mode', () => {
+ expect(serverLinkBuilder('/assets/favicon.ico', null)).toBe('/assets/favicon.ico')
+ })
+
+ test('should handle project references in server mode', () => {
+ const project = { name: 'testproject', id: 123 }
+ expect(serverLinkBuilder('', project)).toBe('/projects/123')
+ })
+
+ test('should prioritize project reference over path', () => {
+ const project = { name: 'testproject', id: 123 }
+ expect(serverLinkBuilder('/some/path', project)).toBe('/projects/123')
+ })
+ })
+})
diff --git a/__tests__/schemas.test.js b/__tests__/schemas.test.js
index b1e52d5..1ab2033 100644
--- a/__tests__/schemas.test.js
+++ b/__tests__/schemas.test.js
@@ -1,5 +1,21 @@
-const { sampleGithubOrg, sampleGithubListOrgRepos, sampleGithubRepository, sampleOSSFScorecardResult, sampleBulkImportFileContent } = require('../__fixtures__')
-const { validateGithubOrg, validateGithubListOrgRepos, validateGithubRepository, validateOSSFResult, validateBulkImport } = require('../src/schemas')
+const {
+ sampleGithubOrg,
+ sampleGithubListOrgRepos,
+ sampleGithubRepository,
+ sampleOSSFScorecardResult,
+ sampleBulkImportFileContent,
+ sampleProjectData,
+ sampleIndexData
+} = require('../__fixtures__')
+const {
+ validateGithubOrg,
+ validateGithubListOrgRepos,
+ validateGithubRepository,
+ validateOSSFResult,
+ validateBulkImport,
+ validateProjectData,
+ validateIndexData
+} = require('../src/schemas')
describe('schemas', () => {
describe('validateGithubOrg', () => {
@@ -81,4 +97,156 @@ describe('schemas', () => {
expect(() => validateBulkImport(invalidData)).toThrow()
})
})
+
+ describe('validateProjectData', () => {
+ test('Should not throw an error with valid data', () => {
+ expect(() => validateProjectData(sampleProjectData)).not.toThrow()
+ })
+
+ test('Should not throw an error with additional data', () => {
+ // Create a valid project data object with additional properties
+ const validProjectDataWithAdditionalProps = {
+ ...sampleProjectData,
+ project: {
+ ...sampleProjectData.project,
+ additionalProperty: 'value'
+ },
+ additionalProperty: 'value'
+ }
+
+ expect(() => validateProjectData(validProjectDataWithAdditionalProps)).not.toThrow()
+ })
+
+ test('Should throw an error with invalid data', () => {
+ // Create an invalid project data object (missing required field)
+ const invalidProjectData = {
+ project: {
+ // Missing id field
+ name: 'Test Project'
+ },
+ checks: [],
+ results: [],
+ tasks: [],
+ alerts: [],
+ githubOrgs: [],
+ githubRepos: [],
+ ossfScorecardResults: []
+ }
+
+ expect(() => validateProjectData(invalidProjectData)).toThrow('Error when validating project data')
+ })
+
+ test('Should throw an error with invalid nested data', () => {
+ // Create an invalid project data object (invalid field type)
+ const invalidProjectData = {
+ project: {
+ id: 1,
+ name: 'Test Project'
+ },
+ checks: [],
+ results: [
+ {
+ id: 1,
+ project_id: 1,
+ name: 'Test result',
+ score: 'not-a-number', // Should be a number
+ created_at: '2023-01-01T00:00:00Z',
+ updated_at: '2023-01-02T00:00:00Z'
+ }
+ ],
+ tasks: [],
+ alerts: [],
+ githubOrgs: [],
+ githubRepos: [],
+ ossfScorecardResults: []
+ }
+
+ expect(() => validateProjectData(invalidProjectData)).toThrow('Error when validating project data')
+ })
+
+ test('Should throw an error when required fields are missing', () => {
+ // Missing required fields in the data object
+ const missingFieldsData = {
+ // Missing project field
+ checks: [],
+ results: [],
+ tasks: [],
+ alerts: [],
+ githubOrgs: [],
+ githubRepos: [],
+ ossfScorecardResults: []
+ }
+
+ expect(() => validateProjectData(missingFieldsData)).toThrow('Error when validating project data')
+ })
+ })
+
+ describe('validateIndexData', () => {
+ test('Should not throw an error with valid data', () => {
+ expect(() => validateIndexData(sampleIndexData)).not.toThrow()
+ })
+
+ test('Should not throw an error with additional data', () => {
+ // Create a valid index data object with additional properties
+ const validIndexDataWithAdditionalProps = {
+ ...sampleIndexData,
+ projects: [
+ {
+ ...sampleIndexData.projects[0],
+ additionalProperty: 'value'
+ }
+ ],
+ additionalProperty: 'value'
+ }
+
+ expect(() => validateIndexData(validIndexDataWithAdditionalProps)).not.toThrow()
+ })
+
+ test('Should throw an error with invalid data', () => {
+ // Create an invalid index data object (missing required field)
+ const invalidIndexData = {
+ projects: [
+ {
+ id: 1,
+ // Missing name field
+ description: 'A test project'
+ }
+ ],
+ checklists: [],
+ checks: []
+ }
+
+ expect(() => validateIndexData(invalidIndexData)).toThrow('Error when validating index data')
+ })
+
+ test('Should throw an error with invalid nested data', () => {
+ // Create an invalid index data object (invalid field type)
+ const invalidIndexData = {
+ projects: [],
+ checklists: [
+ {
+ id: '1', // Should be a number
+ name: 'Test Checklist',
+ type: 'security',
+ created_at: '2023-01-01T00:00:00Z',
+ updated_at: '2023-01-02T00:00:00Z'
+ }
+ ],
+ checks: []
+ }
+
+ expect(() => validateIndexData(invalidIndexData)).toThrow('Error when validating index data')
+ })
+
+ test('Should throw an error when required fields are missing', () => {
+ // Missing required fields in the data object
+ const missingFieldsData = {
+ // Missing projects field
+ checklists: []
+ // Missing checks field
+ }
+
+ expect(() => validateIndexData(missingFieldsData)).toThrow('Error when validating index data')
+ })
+ })
})
diff --git a/package-lock.json b/package-lock.json
index 5138eb7..6687b02 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -27,7 +27,6 @@
"pg": "8.13.1",
"pino": "9.5.0",
"pino-pretty": "13.0.0",
- "serve-index": "1.9.1",
"serve-static": "1.16.2",
"validator": "13.12.0"
},
@@ -2141,19 +2140,6 @@
"dev": true,
"license": "ISC"
},
- "node_modules/accepts": {
- "version": "1.3.8",
- "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
- "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
- "license": "MIT",
- "dependencies": {
- "mime-types": "~2.1.34",
- "negotiator": "0.6.3"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
"node_modules/acorn": {
"version": "8.14.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
@@ -2620,12 +2606,6 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT"
},
- "node_modules/batch": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
- "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==",
- "license": "MIT"
- },
"node_modules/before-after-hook": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz",
@@ -6975,6 +6955,7 @@
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
@@ -6984,6 +6965,7 @@
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
@@ -7045,15 +7027,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/negotiator": {
- "version": "0.6.3",
- "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
- "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
"node_modules/nock": {
"version": "14.0.1",
"resolved": "https://registry.npmjs.org/nock/-/nock-14.0.1.tgz",
@@ -8443,84 +8416,6 @@
"node": ">= 0.8"
}
},
- "node_modules/serve-index": {
- "version": "1.9.1",
- "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz",
- "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==",
- "license": "MIT",
- "dependencies": {
- "accepts": "~1.3.4",
- "batch": "0.6.1",
- "debug": "2.6.9",
- "escape-html": "~1.0.3",
- "http-errors": "~1.6.2",
- "mime-types": "~2.1.17",
- "parseurl": "~1.3.2"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/serve-index/node_modules/debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
- "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
- "license": "MIT",
- "dependencies": {
- "ms": "2.0.0"
- }
- },
- "node_modules/serve-index/node_modules/depd": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
- "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/serve-index/node_modules/http-errors": {
- "version": "1.6.3",
- "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
- "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==",
- "license": "MIT",
- "dependencies": {
- "depd": "~1.1.2",
- "inherits": "2.0.3",
- "setprototypeof": "1.1.0",
- "statuses": ">= 1.4.0 < 2"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/serve-index/node_modules/inherits": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
- "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==",
- "license": "ISC"
- },
- "node_modules/serve-index/node_modules/ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
- "license": "MIT"
- },
- "node_modules/serve-index/node_modules/setprototypeof": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
- "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==",
- "license": "ISC"
- },
- "node_modules/serve-index/node_modules/statuses": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
- "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
"node_modules/serve-static": {
"version": "1.16.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
diff --git a/package.json b/package.json
index 971aa03..e498448 100644
--- a/package.json
+++ b/package.json
@@ -55,7 +55,6 @@
"pg": "8.13.1",
"pino": "9.5.0",
"pino-pretty": "13.0.0",
- "serve-index": "1.9.1",
"serve-static": "1.16.2",
"validator": "13.12.0"
},
diff --git a/server.js b/server.js
index e74a82a..b908de2 100644
--- a/server.js
+++ b/server.js
@@ -13,11 +13,26 @@ const server = require('./src/httpServer');
// Handle graceful shutdown
const shutdown = async (signal) => {
console.log(`Received ${signal}, shutting down gracefully...`)
+ if (!serverInstance) {
+ console.log('No server instance running, exiting')
+ process.exit(0)
+ return
+ }
+
+ // Set a hard timeout to force exit if graceful shutdown takes too long
+ const forceExitTimeout = setTimeout(() => {
+ console.error('Shutdown timed out after 10 seconds, forcing exit')
+ process.exit(1)
+ }, 10000)
+
try {
await serverInstance.stop()
+ clearTimeout(forceExitTimeout)
console.log('Server stopped successfully')
- process.exit(0)
+ // Small delay to ensure logs are flushed
+ setTimeout(() => process.exit(0), 100)
} catch (error) {
+ clearTimeout(forceExitTimeout)
console.error('Error during shutdown:', error)
process.exit(1)
}
diff --git a/src/httpServer/index.js b/src/httpServer/index.js
index 3472305..765a7a1 100644
--- a/src/httpServer/index.js
+++ b/src/httpServer/index.js
@@ -1,30 +1,36 @@
const express = require('express')
const http = require('http')
const serveStatic = require('serve-static')
-const serveIndex = require('serve-index')
const { join } = require('path')
const { getConfig } = require('../config')
const { logger, checkDatabaseConnection } = require('../utils')
-const { createApiRouter } = require('./apiV1')
-
-const publicPath = join(process.cwd(), 'output')
+const { createApiRouter } = require('./routers/apiV1')
+const { createWebRouter } = require('./routers/website')
const { staticServer, dbSettings } = getConfig()
const knex = require('knex')(dbSettings)
// Create Express app
const app = express()
+const basePath = join(__dirname, '..', 'reports')
+const publicPath = join(basePath, 'assets')
+const templatePath = join(basePath, 'templates')
+
+// Middleware
+app.set('view engine', 'ejs')
+app.set('views', templatePath)
// API Routes
app.use('/api/v1', createApiRouter(knex, express))
-// Static file serving
-app.use(serveStatic(publicPath, {
- index: false,
- dotfiles: 'deny'
-}))
+// Web Routes
+app.use('/', createWebRouter(knex, express))
// Directory listing for static files
-app.use(serveIndex(publicPath, { icons: true }))
+app.use('/assets', serveStatic(publicPath, {
+ index: false,
+ dotfiles: 'deny',
+ icons: true
+}))
// Error handling middleware
app.use((err, req, res, next) => {
@@ -38,6 +44,9 @@ app.use((err, req, res, next) => {
// Create HTTP server
const server = http.createServer(app)
+// Track all active connections
+const connections = new Set()
+
module.exports = () => ({
start: async () => {
const isDbConnected = await checkDatabaseConnection(knex)
@@ -54,19 +63,66 @@ module.exports = () => ({
resolve()
})
})
+
+ // Track connections
+ server.on('connection', connection => {
+ connections.add(connection)
+ connection.on('close', () => {
+ connections.delete(connection)
+ })
+ })
return server
},
stop: async () => {
- await knex.destroy()
- await new Promise((resolve, reject) => {
- server.close(err => {
- if (err) {
- logger.error('Failed to stop server', { err })
- return reject(err)
+ logger.info('Starting server shutdown sequence')
+ try {
+ // Check if server is listening before attempting to close
+ if (!server.listening) {
+ logger.info('Server was not listening, skipping server.close()')
+ } else {
+ logger.info('Closing HTTP server - stop accepting new connections')
+ // Terminate all existing connections if needed
+ if (connections.size > 0) {
+ logger.info(`${connections.size} active connections will be allowed to complete`)
}
- logger.info('Server stopped')
- resolve()
- })
- })
+
+ // First, stop accepting new connections and wait for existing ones to complete
+ await new Promise((resolve, reject) => {
+ // Add a timeout to prevent hanging indefinitely
+ const timeout = setTimeout(() => {
+ logger.warn('Server close timed out after 5 seconds, forcing active connections to close')
+ // Force close any remaining connections
+ if (connections.size > 0) {
+ logger.info(`Forcibly closing ${connections.size} remaining connections`)
+ for (const connection of connections) {
+ connection.destroy()
+ }
+ connections.clear()
+ }
+ resolve()
+ }, 5000)
+
+ server.close(err => {
+ clearTimeout(timeout)
+ if (err) {
+ logger.error('Failed to stop server', { err })
+ return reject(err)
+ }
+ logger.info('HTTP server closed successfully')
+ resolve()
+ })
+ })
+ }
+
+ // Only after the server is closed, close database connections
+ logger.info('Closing database connections')
+ await knex.destroy()
+ logger.info('Database connections closed')
+
+ logger.info('Server shutdown complete')
+ } catch (error) {
+ logger.error('Error during server shutdown', { error })
+ throw error
+ }
}
})
diff --git a/src/httpServer/apiV1.js b/src/httpServer/routers/apiV1.js
similarity index 88%
rename from src/httpServer/apiV1.js
rename to src/httpServer/routers/apiV1.js
index cfdfeaa..0894c0d 100644
--- a/src/httpServer/apiV1.js
+++ b/src/httpServer/routers/apiV1.js
@@ -1,5 +1,5 @@
-const { generateStaticReports } = require('../reports')
-const { logger } = require('../utils')
+const { generateStaticReports } = require('../../reports')
+const { logger } = require('../../utils')
function createApiRouter (knex, express) {
const router = express.Router()
diff --git a/src/httpServer/routers/website.js b/src/httpServer/routers/website.js
new file mode 100644
index 0000000..625ed64
--- /dev/null
+++ b/src/httpServer/routers/website.js
@@ -0,0 +1,45 @@
+const { logger } = require('../../utils')
+const { collectIndexData, collectProjectData } = require('../../reports')
+const { initializeStore } = require('../../store')
+
+const indexTemplate = 'index'
+const projectTemplate = 'project'
+const notFoundTemplate = 'notFound'
+
+function createWebRouter (knex, express) {
+ const router = express.Router()
+ const { getProjectById } = initializeStore(knex)
+
+ router.get('/', async (req, res) => {
+ try {
+ const data = await collectIndexData(knex)
+ res.render(indexTemplate, data)
+ } catch (error) {
+ logger.error(error)
+ res.status(500).send('Error rendering index page')
+ }
+ })
+
+ router.get('/projects/:id', async (req, res) => {
+ const projectId = parseInt(req.params.id)
+ if (isNaN(projectId)) {
+ logger.error(`Invalid project ID: ${req.params.id}`)
+ return res.status(404).render(notFoundTemplate)
+ }
+ try {
+ const project = await getProjectById(projectId)
+ if (!project) {
+ logger.error(`Project not found: ${projectId}`)
+ return res.status(404).render(notFoundTemplate)
+ }
+ const data = await collectProjectData(knex, projectId)
+ res.render(projectTemplate, data)
+ } catch (error) {
+ logger.error(error)
+ res.status(500).send('Error rendering project page')
+ }
+ })
+ return router
+}
+
+module.exports = { createWebRouter }
diff --git a/src/reports/assets/logo.png b/src/reports/assets/logo.png
new file mode 100644
index 0000000..29a82a5
Binary files /dev/null and b/src/reports/assets/logo.png differ
diff --git a/src/reports/index.js b/src/reports/index.js
index d233521..6d71099 100644
--- a/src/reports/index.js
+++ b/src/reports/index.js
@@ -3,9 +3,10 @@ const ejs = require('ejs')
const { mkdir, readdir, copyFile, readFile, writeFile, rm } = require('node:fs').promises
const { join } = require('path')
const { initializeStore } = require('../store')
+const { validateProjectData, validateIndexData } = require('../schemas')
-const indexTemplatePath = join(process.cwd(), 'src', 'reports', 'templates', 'index.html.ejs')
-const projectTemplatePath = join(process.cwd(), 'src', 'reports', 'templates', 'project.html.ejs')
+const indexTemplatePath = join(process.cwd(), 'src', 'reports', 'templates', 'index.ejs')
+const projectTemplatePath = join(process.cwd(), 'src', 'reports', 'templates', 'project.ejs')
const assetsFolder = join(process.cwd(), 'src', 'reports', 'assets')
const destinationFolder = join(process.cwd(), 'output')
const copyFolder = async (from, to) => {
@@ -37,6 +38,90 @@ const copyFolder = async (from, to) => {
}
}
+const collectIndexData = async (knex) => {
+ const { getAllProjects, getAllChecklists, getAllComplianceChecks } = initializeStore(knex)
+ try {
+ const [projects, checklists, checks] = await Promise.all([
+ getAllProjects(),
+ getAllChecklists(),
+ getAllComplianceChecks()
+ ])
+ const getLink = internalLinkBuilder('server')
+
+ // Create the data object
+ const data = { projects, checklists, checks, getLink }
+
+ // Validate the data against the schema
+ validateIndexData(data)
+
+ return data
+ } catch (error) {
+ logger.error(`Error collecting index data: ${error.message}`)
+ throw error
+ }
+}
+
+// @TODO: use new store functions to collect project data individually more accurately and avoid loops
+const collectProjectData = async (knex, projectId) => {
+ const { getAllComplianceChecks, getProjectById, getAllGithubRepositories, getAllOSSFResults, getAllAlerts, getAllResults, getAllTasks, getAllGithubOrganizationsByProjectsId } = initializeStore(knex)
+ try {
+ const project = await getProjectById(projectId)
+ if (!project) {
+ throw new Error(`Project not found: ${projectId}`)
+ }
+ const [checks, ossfScorecardResults, alerts, results, tasks, githubRepos, githubOrgs] = await Promise.all([
+ getAllComplianceChecks(),
+ getAllOSSFResults(),
+ getAllAlerts(),
+ getAllResults(),
+ getAllTasks(),
+ getAllGithubRepositories(),
+ getAllGithubOrganizationsByProjectsId([projectId])
+ ])
+
+ // Process the data
+ const githubOrgsIds = githubOrgs.map(org => org.id)
+ const githubReposInScope = githubRepos.filter(repo => githubOrgsIds.includes(repo.github_organization_id))
+ const githubReposInScopeIds = githubReposInScope.map(repo => repo.id)
+ const getLink = internalLinkBuilder('server')
+
+ // Create the data object
+ const data = {
+ project,
+ checks,
+ alerts: alerts.filter(alert => alert.project_id === project.id),
+ results: results.filter(result => result.project_id === project.id),
+ tasks: tasks.filter(task => task.project_id === project.id),
+ githubOrgs,
+ githubRepos: githubReposInScope,
+ ossfScorecardResults: ossfScorecardResults.filter(ossfResult => githubReposInScopeIds.includes(ossfResult.github_repository_id)),
+ getLink
+ }
+
+ // Validate the data against the schema
+ validateProjectData(data)
+
+ return data
+ } catch (error) {
+ logger.error(`Error collecting project data: ${error.message}`)
+ throw error
+ }
+}
+
+const internalLinkBuilder = (mode = 'static') => (ref, project) => {
+ let finalRef = ref
+ // remove leading slash
+ if (mode === 'static') {
+ finalRef = finalRef.replace(/^\//, '')
+ }
+ // project specific paths
+ if (project) {
+ finalRef = mode === 'static' ? `${project.name}.html` : `/projects/${project.id}`
+ }
+
+ return finalRef.length > 0 ? finalRef : 'index.html'
+}
+
const generateStaticReports = async (knex, options = { clearPreviousReports: false }) => {
const { clearPreviousReports } = options
if (clearPreviousReports) {
@@ -67,19 +152,24 @@ const generateStaticReports = async (knex, options = { clearPreviousReports: fal
getAllGithubRepositories()
])
- // @TODO: Read the files in parallel
- const indexTemplate = await readFile(indexTemplatePath, 'utf8')
- const projectTemplate = await readFile(projectTemplatePath, 'utf8')
- await mkdir(join(destinationFolder, 'projects'), { recursive: true })
+ const [indexTemplate, projectTemplate] = await Promise.all([
+ readFile(indexTemplatePath, 'utf8'),
+ readFile(projectTemplatePath, 'utf8')
+ ])
+ await mkdir(destinationFolder, { recursive: true })
// Collecting data from the database
const indexData = {
projects,
checklists,
- checks
+ checks,
+ getLink: internalLinkBuilder('static')
}
- const projectsData = {}
+ // Validate index data
+ validateIndexData(indexData)
+
+ const projectsData = { }
for (const project of projects) {
const githubOrgs = await getAllGithubOrganizationsByProjectsId([project.id])
@@ -87,7 +177,7 @@ const generateStaticReports = async (knex, options = { clearPreviousReports: fal
const githubReposInScope = githubRepos.filter(repo => githubOrgsIds.includes(repo.github_organization_id))
const githubReposInScopeIds = githubReposInScope.map(repo => repo.id)
- projectsData[project.name] = {
+ const projectData = {
project,
checks,
alerts: alerts.filter(alert => alert.project_id === project.id),
@@ -95,26 +185,41 @@ const generateStaticReports = async (knex, options = { clearPreviousReports: fal
tasks: tasks.filter(task => task.project_id === project.id),
githubOrgs,
githubRepos: githubReposInScope,
- ossfScorecardResults: ossfScorecardResults.filter(ossfResult => githubReposInScopeIds.includes(ossfResult.github_repository_id))
+ ossfScorecardResults: ossfScorecardResults.filter(ossfResult => githubReposInScopeIds.includes(ossfResult.github_repository_id)),
+ getLink: internalLinkBuilder('static')
}
+ // Validate each project's data
+ validateProjectData(projectData)
+
+ projectsData[project.name] = projectData
+
// Populate the project HTML template
- const projectHtml = ejs.render(projectTemplate, projectsData[project.name])
- const projectFilename = join(destinationFolder, 'projects', `${project.name}.html`)
- await writeFile(projectFilename, projectHtml)
+ const projectHtml = ejs.render(projectTemplate, projectsData[project.name], {
+ filename: projectTemplatePath,
+ views: [join(process.cwd(), 'src', 'reports', 'templates')]
+ })
+ // @TODO: Prevent overwriting (edge case) at creation level
+ if (project.name !== 'index') {
+ const safeName = encodeURIComponent(project.name)
+ const projectFilename = join(destinationFolder, `${safeName}.html`)
+ await writeFile(projectFilename, projectHtml)
+ }
}
- // @TODO: Validate against JSON Schemas
-
- // @TODO: Save the files in parallel
- await writeFile('output/index_data.json', JSON.stringify(indexData, null, 2))
- await writeFile('output/projects/projects_data.json', JSON.stringify(projectsData, null, 2))
+ await Promise.all([
+ writeFile('output/index_data.json', JSON.stringify(indexData, null, 2)),
+ writeFile('output/projects_data.json', JSON.stringify(projectsData, null, 2))
+ ])
// copy assets folder
await copyFolder(assetsFolder, join(destinationFolder, 'assets'))
// Populate the index HTML template
- const indexHtml = ejs.render(indexTemplate, indexData)
+ const indexHtml = ejs.render(indexTemplate, indexData, {
+ filename: indexTemplatePath,
+ views: [join(process.cwd(), 'src', 'reports', 'templates')]
+ })
// Save the index HTML file
await writeFile('output/index.html', indexHtml)
@@ -122,5 +227,8 @@ const generateStaticReports = async (knex, options = { clearPreviousReports: fal
}
module.exports = {
- generateStaticReports
+ generateStaticReports,
+ collectIndexData,
+ collectProjectData,
+ internalLinkBuilder
}
diff --git a/src/reports/templates/index.html.ejs b/src/reports/templates/index.ejs
similarity index 85%
rename from src/reports/templates/index.html.ejs
rename to src/reports/templates/index.ejs
index ce1d3aa..36015ba 100644
--- a/src/reports/templates/index.html.ejs
+++ b/src/reports/templates/index.ejs
@@ -1,24 +1,16 @@
+<% function resolveLink(path, data) { return typeof getLink !== 'function' ? path : getLink(path, data); } %>
-
-
- VisionBoard Reports
-
-
+ <%- include('partials/head') %>
-
-
-
-
-
VisionBoard Reports
-
-
+
+ <%- include('partials/navigation') %>
-
-
+
+
Welcome!
In this dashboard, you can find the status of all the projects that you
registered. Every project listed includes additional reports with its own dashboard, tasks, and alerts.
@@ -29,7 +21,7 @@
+
+ <%- include('partials/footer') %>
+
\ No newline at end of file
diff --git a/src/reports/templates/notFound.ejs b/src/reports/templates/notFound.ejs
new file mode 100644
index 0000000..2f5387a
--- /dev/null
+++ b/src/reports/templates/notFound.ejs
@@ -0,0 +1,23 @@
+
+
+
+
+ <%- include('partials/head') %>
+
+
+
+ <%- include('partials/navigation') %>
+
+
+
+ <%- include('partials/footer') %>
+
+
+
+
\ No newline at end of file
diff --git a/src/reports/templates/partials/footer.ejs b/src/reports/templates/partials/footer.ejs
new file mode 100644
index 0000000..80e6b5a
--- /dev/null
+++ b/src/reports/templates/partials/footer.ejs
@@ -0,0 +1,5 @@
+
+
+
VisionBoard. MIT License.
+
+
diff --git a/src/reports/templates/partials/head.ejs b/src/reports/templates/partials/head.ejs
new file mode 100644
index 0000000..22a431c
--- /dev/null
+++ b/src/reports/templates/partials/head.ejs
@@ -0,0 +1,7 @@
+
+
+VisionBoard Reports
+<% function resolveLink(path) { return typeof getLink !== 'function' ? path : getLink(path); } %>
+
+
+
\ No newline at end of file
diff --git a/src/reports/templates/partials/navigation.ejs b/src/reports/templates/partials/navigation.ejs
new file mode 100644
index 0000000..61caa8f
--- /dev/null
+++ b/src/reports/templates/partials/navigation.ejs
@@ -0,0 +1,11 @@
+
+<% function resolveLink(path) { return typeof getLink !== 'function' ? path : getLink(path); } %>
+
+
\ No newline at end of file
diff --git a/src/reports/templates/project.html.ejs b/src/reports/templates/project.ejs
similarity index 94%
rename from src/reports/templates/project.html.ejs
rename to src/reports/templates/project.ejs
index e3cb363..5a89318 100644
--- a/src/reports/templates/project.html.ejs
+++ b/src/reports/templates/project.ejs
@@ -2,22 +2,13 @@
-
-
- VisionBoard Reports
-
-
+ <%- include('partials/head') %>
-
-
-
-
-
VisionBoard Reports
-
-
-
-
+
+ <%- include('partials/navigation') %>
+
+
<%= project.name %> Report
@@ -245,5 +236,8 @@
<% } %>
+
+ <%- include('partials/footer') %>
+
\ No newline at end of file
diff --git a/src/schemas/index.js b/src/schemas/index.js
index 5bede21..252fc46 100644
--- a/src/schemas/index.js
+++ b/src/schemas/index.js
@@ -5,8 +5,12 @@ const githubListOrgReposSchema = require('./githubListOrgRepos.json')
const githubRepositorySchema = require('./githubRepository.json')
const ossfScorecardResultSchema = require('./ossfScorecardResult.json')
const bulkImportSchema = require('./bulkImport.json')
+const projectDataSchema = require('./projectData.json')
+const indexDataSchema = require('./indexData.json')
-const ajv = new Ajv()
+const ajv = new Ajv({
+ allowUnionTypes: true // Allow union types for fields like Date/string
+})
addFormats(ajv)
const getReadableErrors = validate => validate.errors.map((error) => `[ERROR: ${error.keyword}]${error.schemaPath}: ${error.message}`).join('\n')
@@ -61,10 +65,32 @@ const validateBulkImport = (data) => {
return null
}
+const validateProjectData = (data) => {
+ const validate = ajv.compile(projectDataSchema)
+ const valid = validate(data)
+ if (!valid) {
+ const readableErrors = getReadableErrors(validate)
+ throw new Error(`Error when validating project data: ${readableErrors}`)
+ }
+ return null
+}
+
+const validateIndexData = (data) => {
+ const validate = ajv.compile(indexDataSchema)
+ const valid = validate(data)
+ if (!valid) {
+ const readableErrors = getReadableErrors(validate)
+ throw new Error(`Error when validating index data: ${readableErrors}`)
+ }
+ return null
+}
+
module.exports = {
validateGithubOrg,
validateGithubListOrgRepos,
validateGithubRepository,
validateOSSFResult,
- validateBulkImport
+ validateBulkImport,
+ validateProjectData,
+ validateIndexData
}
diff --git a/src/schemas/indexData.json b/src/schemas/indexData.json
new file mode 100644
index 0000000..46a08ec
--- /dev/null
+++ b/src/schemas/indexData.json
@@ -0,0 +1,52 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "required": ["projects", "checklists", "checks"],
+ "properties": {
+ "projects": {
+ "type": "array",
+ "minItems": 0,
+ "items": {
+ "type": "object",
+ "required": ["id", "name"],
+ "properties": {
+ "id": { "type": "integer" },
+ "name": { "type": "string" },
+ "description": { "type": ["string", "null"] },
+ "created_at": { "type": ["string", "object"], "format": "date-time" },
+ "updated_at": { "type": ["string", "object"], "format": "date-time" }
+ }
+ }
+ },
+ "checklists": {
+ "type": "array",
+ "minItems": 0,
+ "items": {
+ "type": "object",
+ "required": ["id"],
+ "properties": {
+ "id": { "type": "integer" },
+ "name": { "type": "string" },
+ "type": { "type": "string" },
+ "created_at": { "type": ["string", "object"], "format": "date-time" },
+ "updated_at": { "type": ["string", "object"], "format": "date-time" }
+ }
+ }
+ },
+ "checks": {
+ "type": "array",
+ "minItems": 0,
+ "items": {
+ "type": "object",
+ "required": ["id"],
+ "properties": {
+ "id": { "type": "integer" },
+ "checklist_id": { "type": "integer" },
+ "description": { "type": "string" },
+ "created_at": { "type": ["string", "object"], "format": "date-time" },
+ "updated_at": { "type": ["string", "object"], "format": "date-time" }
+ }
+ }
+ }
+ }
+}
diff --git a/src/schemas/projectData.json b/src/schemas/projectData.json
new file mode 100644
index 0000000..586e271
--- /dev/null
+++ b/src/schemas/projectData.json
@@ -0,0 +1,127 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "required": ["project", "checks", "results", "tasks", "alerts", "githubOrgs", "githubRepos", "ossfScorecardResults"],
+ "properties": {
+ "project": {
+ "type": "object",
+ "required": ["id", "name"],
+ "properties": {
+ "id": { "type": "integer" },
+ "name": { "type": "string" },
+ "description": { "type": ["string", "null"] },
+ "created_at": { "type": ["string", "object"], "format": "date-time" },
+ "updated_at": { "type": ["string", "object"], "format": "date-time" }
+ }
+ },
+ "checks": {
+ "type": "array",
+ "minItems": 0,
+ "items": {
+ "type": "object",
+ "required": ["id"],
+ "properties": {
+ "id": { "type": "integer" },
+ "checklist_id": { "type": "integer" },
+ "description": { "type": "string" },
+ "created_at": { "type": ["string", "object"], "format": "date-time" },
+ "updated_at": { "type": ["string", "object"], "format": "date-time" }
+ }
+ }
+ },
+ "results": {
+ "type": "array",
+ "minItems": 0,
+ "items": {
+ "type": "object",
+ "required": ["id", "project_id", "name", "score"],
+ "properties": {
+ "id": { "type": "integer" },
+ "project_id": { "type": "integer" },
+ "name": { "type": "string" },
+ "score": { "type": "number" },
+ "created_at": { "type": ["string", "object"], "format": "date-time" },
+ "updated_at": { "type": ["string", "object"], "format": "date-time" }
+ }
+ }
+ },
+ "tasks": {
+ "type": "array",
+ "minItems": 0,
+ "items": {
+ "type": "object",
+ "required": ["id", "project_id", "description", "implementation_status"],
+ "properties": {
+ "id": { "type": "integer" },
+ "project_id": { "type": "integer" },
+ "description": { "type": "string" },
+ "implementation_status": { "type": "string", "enum": ["pending", "in_progress", "completed"] },
+ "created_at": { "type": ["string", "object"], "format": "date-time" },
+ "updated_at": { "type": ["string", "object"], "format": "date-time" }
+ }
+ }
+ },
+ "alerts": {
+ "type": "array",
+ "minItems": 0,
+ "items": {
+ "type": "object",
+ "required": ["id", "project_id", "title", "description", "severity"],
+ "properties": {
+ "id": { "type": "integer" },
+ "project_id": { "type": "integer" },
+ "title": { "type": "string" },
+ "description": { "type": "string" },
+ "severity": { "type": "string", "enum": ["low", "medium", "high", "critical"] },
+ "created_at": { "type": ["string", "object"], "format": "date-time" },
+ "updated_at": { "type": ["string", "object"], "format": "date-time" }
+ }
+ }
+ },
+ "githubOrgs": {
+ "type": "array",
+ "minItems": 0,
+ "items": {
+ "type": "object",
+ "required": ["id", "login"],
+ "properties": {
+ "id": { "type": "integer" },
+ "name": { "type": ["string", "null"] },
+ "login": { "type": "string" },
+ "created_at": { "type": ["string", "object"], "format": "date-time" },
+ "updated_at": { "type": ["string", "object"], "format": "date-time" }
+ }
+ }
+ },
+ "githubRepos": {
+ "type": "array",
+ "minItems": 0,
+ "items": {
+ "type": "object",
+ "required": ["id", "name", "full_name"],
+ "properties": {
+ "id": { "type": "integer" },
+ "name": { "type": "string" },
+ "full_name": { "type": "string" },
+ "created_at": { "type": ["string", "object"], "format": "date-time" },
+ "updated_at": { "type": ["string", "object"], "format": "date-time" }
+ }
+ }
+ },
+ "ossfScorecardResults": {
+ "type": "array",
+ "minItems": 0,
+ "items": {
+ "type": "object",
+ "required": ["id"],
+ "properties": {
+ "id": { "type": "integer" },
+ "github_repository_id": { "type": "integer" },
+ "score": { "type": "number" },
+ "created_at": { "type": ["string", "object"], "format": "date-time" },
+ "updated_at": { "type": ["string", "object"], "format": "date-time" }
+ }
+ }
+ }
+ }
+}
diff --git a/src/store/index.js b/src/store/index.js
index acb1a88..549bc59 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -11,6 +11,11 @@ const addFn = knex => (table, record) => {
return knex(table).insert(record).returning('*').then(results => results[0])
}
+const getFn = knex => (table, id) => {
+ debug(`Fetching record ${id} from ${table}...`)
+ return knex(table).where({ id }).first()
+}
+
const upsertRecord = async ({ knex, table, uniqueCriteria, data }) => {
const existingRecord = await knex(table).where(uniqueCriteria).first()
if (existingRecord) {
@@ -199,6 +204,7 @@ const initializeStore = (knex) => {
debug('Initializing store...')
const getAll = getAllFn(knex)
const addTo = addFn(knex)
+ const getOne = getFn(knex)
return {
addProject: addProject(knex),
addGithubOrganization: addGithubOrganization(knex),
@@ -234,7 +240,8 @@ const initializeStore = (knex) => {
upsertSoftwareDesignTraining: upsertSoftwareDesignTraining(knex),
upsertProjectPolicies: upsertProjectPolicies(knex),
upsertOwaspTop10Training: upsertOwaspTop10Training(knex),
- getAllOSSFResults: () => getAll('ossf_scorecard_results')
+ getAllOSSFResults: () => getAll('ossf_scorecard_results'),
+ getProjectById: (id) => getOne('projects', id)
}
}