121 lines
3.1 KiB
JavaScript
121 lines
3.1 KiB
JavaScript
import path from 'path'
|
|
import { fileURLToPath } from 'url'
|
|
import glob from 'glob'
|
|
import countBy from 'lodash.countby'
|
|
import categories from '../../services/categories.js'
|
|
import BaseService from './base.js'
|
|
import { assertValidServiceDefinitionExport } from './service-definitions.js'
|
|
|
|
const serviceDir = path.join(
|
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
'..',
|
|
'..',
|
|
'services'
|
|
)
|
|
|
|
function toUnixPath(path) {
|
|
// glob does not allow \ as a path separator
|
|
// see https://github.com/isaacs/node-glob/blob/main/changelog.md#80
|
|
// so we need to convert to use / for use with glob
|
|
return path.replace(/\\/g, '/')
|
|
}
|
|
|
|
class InvalidService extends Error {
|
|
constructor(message) {
|
|
super(message)
|
|
this.name = 'InvalidService'
|
|
}
|
|
}
|
|
|
|
function getServicePaths(pattern) {
|
|
return glob.sync(toUnixPath(path.join(serviceDir, '**', pattern)))
|
|
}
|
|
|
|
async function loadServiceClasses(servicePaths) {
|
|
if (!servicePaths) {
|
|
servicePaths = getServicePaths('*.service.js')
|
|
}
|
|
|
|
const serviceClasses = []
|
|
for await (const servicePath of servicePaths) {
|
|
const currentServiceClasses = Object.values(
|
|
await import(`file://${servicePath}`)
|
|
).flatMap(element =>
|
|
typeof element === 'object' ? Object.values(element) : element
|
|
)
|
|
|
|
if (currentServiceClasses.length === 0) {
|
|
throw new InvalidService(
|
|
`Expected ${servicePath} to export a service or a collection of services`
|
|
)
|
|
}
|
|
currentServiceClasses.forEach(serviceClass => {
|
|
if (serviceClass && serviceClass.prototype instanceof BaseService) {
|
|
// Decorate each service class with the directory that contains it.
|
|
serviceClass.serviceFamily = servicePath
|
|
.replace(toUnixPath(serviceDir), '')
|
|
.split('/')[1]
|
|
serviceClass.validateDefinition()
|
|
return serviceClasses.push(serviceClass)
|
|
}
|
|
throw new InvalidService(
|
|
`Expected ${servicePath} to export a service or a collection of services; one of them was ${serviceClass}`
|
|
)
|
|
})
|
|
}
|
|
|
|
return serviceClasses
|
|
}
|
|
|
|
function assertNamesUnique(names, { message }) {
|
|
const duplicates = {}
|
|
Object.entries(countBy(names))
|
|
.filter(([name, count]) => count > 1)
|
|
.forEach(([name, count]) => {
|
|
duplicates[name] = count
|
|
})
|
|
if (Object.keys(duplicates).length) {
|
|
throw new Error(`${message}: ${JSON.stringify(duplicates, undefined, 2)}`)
|
|
}
|
|
}
|
|
|
|
async function checkNames() {
|
|
const services = await loadServiceClasses()
|
|
assertNamesUnique(
|
|
services.map(({ name }) => name),
|
|
{
|
|
message: 'Duplicate service names found',
|
|
}
|
|
)
|
|
}
|
|
|
|
async function collectDefinitions() {
|
|
const services = (await loadServiceClasses())
|
|
// flatMap.
|
|
.map(ServiceClass => ServiceClass.getDefinition())
|
|
.reduce((accum, these) => accum.concat(these), [])
|
|
|
|
const result = { schemaVersion: '0', categories, services }
|
|
|
|
assertValidServiceDefinitionExport(result)
|
|
|
|
return result
|
|
}
|
|
|
|
async function loadTesters() {
|
|
return Promise.all(
|
|
getServicePaths('*.tester.js').map(
|
|
async path => await import(`file://${path}`)
|
|
)
|
|
)
|
|
}
|
|
|
|
export {
|
|
InvalidService,
|
|
loadServiceClasses,
|
|
getServicePaths,
|
|
checkNames,
|
|
collectDefinitions,
|
|
loadTesters,
|
|
}
|