hyperdb-helper
Version:
A wee little helper cli for working with hyperdb schemas
429 lines (368 loc) • 12.8 kB
JavaScript
import * as fs from 'fs/promises'
import * as path from 'path'
import dedent from 'string-dedent'
import Hyperschema from 'hyperschema'
import HyperDB from 'hyperdb/builder/index.js'
import * as templates from './templates/basic.js'
import * as examples from './templates/examples.js'
export class HyperdbHelper {
static defaultConfig = {
databaseConfigDirectory: './database',
generatedCodeDirectory: './generated',
functionsFilepath: './functions.js',
schemaFilepath: './schema.js',
configFilepath: './config.js',
projectPackageJsonFilepath: './package.json',
package: {}
}
/**
* Hyperdb Helper - A utility class for managing Hyperdb database schemas
* @class
* @classdesc Provides methods for initializing and building Hyperdb databases from schema files
*
* @property {Object} options - Configuration options merged with defaults
* @property {Object} config - Current active configuration
* @property {string[]} requiredDependencies - Required npm dependencies
*
* @example
* const helper = new HyperdbHelper();
* await helper.init();
* await helper.build();
*/
constructor(options = {}) {
this.config = { ...HyperdbHelper.defaultConfig, ...options }
this.requiredDependencies = ['hyperschema', 'hyperdb', 'corestore']
}
/**
* Initializes a new database schema directory with required configuration files
* @async
* @param {string} filepath - Path where the database schema directory should be created
* @param {Object} options - Configuration options
* @param {boolean} [options.examples] - Whether to include example code in generated files
* @returns {Promise<Object>} Result object
* @returns {string[]} result.dependenciesNeeded - Array of npm package names that need to be installed
* @throws {Error} If package.json is missing from project directory
* @throws {Error} If schema directory already exists
* @throws {Error} If example index.js file already exists when using examples flag
* @example
* const helper = new HyperdbHelper();
* const { dependenciesNeeded } = await helper.init('./mydb', { examples: true });
* if (dependenciesNeeded.length) {
* console.log('Please install:', dependenciesNeeded.join(' '));
* }
*/
async init(filepath, options) {
this.config = await this.mergeConfig(filepath, options)
this.validateConfig(this.config) // Add validation here
if (!(await exists('./package.json'))) {
console.log(dedent`
Please first create a package.json file in your project directory
and choose either "module" or "commonjs" as the "type" field
You can create a package.json file by running:
npm init
`)
throw new Error('Error: package.json not found in project directory')
}
await this.createDefaultFiles()
const dependenciesNeeded = this.checkPackageDependencies()
return { dependenciesNeeded }
}
/**
* Builds a database from schema files in the configured directory
* @async
* @param {string} filepath - Path to the database directory
* @param {Object} options - Configuration options
* @param {boolean} [options.examples] - Whether to include example code
* @throws {Error} If schema directory does not exist
* @throws {Error} If schema files cannot be loaded or processed
* @returns {Promise<void>}
* @example
* const helper = new HyperdbHelper();
* await helper.build('./mydb', { examples: true });
* // Generated database code will be in ./mydb/generated/
*/
async build(filepath, options) {
this.config = await this.mergeConfig(filepath, options)
if (!(await exists(this.config.databaseConfigDirectory))) {
throw new Error(dedent`
Error: Database directory not found at ${this.config.databaseConfigDirectory}
Run 'hyperdb-helper init' to create a new schema directory
`)
}
const schemaDefinitions = await this.getSchemaDefinitions(
this.config.schemaFilepath
)
await this.buildSchema(schemaDefinitions)
await this.createDatabaseConfigPackageJsonFile()
await this.createGeneratedPackageJsonFile()
}
/**
* Cleans up generated files and resources
* @async
* @method
* @description Removes the generated code directory and any temporary files created during the build process
* @throws {Error} If cleanup operation fails
* @returns {Promise<void>}
* @example
* const helper = new HyperdbHelper();
* await helper.init();
* await helper.build();
* // When done with the helper
* await helper.cleanup();
*/
async cleanup() {
try {
// Cleanup temporary files/resources
await fs.rm(this.config.generatedCodeDirectory, {
recursive: true,
force: true
})
} catch (error) {
this.logger.warn('Cleanup failed:', error)
}
}
validateConfig(config) {
const required = [
// Core directories
'databaseConfigDirectory', // Where the schema configuration lives
'generatedCodeDirectory', // Where generated code will be output
// Essential file paths for schema definition
'functionsFilepath', // Custom functions for the schema
'schemaFilepath', // The main schema definition file
'configFilepath', // Configuration file path
// Package.json related paths
'projectPackageJsonFilepath', // Project's package.json
'databaseConfigJsonFilepath', // Database config package.json
'generatedPackageJsonFilepath', // Generated code package.json
// Generated code directories
'hyperschemaDirectory', // Where schema definitions are generated
'hyperdbDirectory', // Where database code is generated
// Module configuration
'moduleType' // 'commonjs' or 'module'
]
const missing = required.filter((field) => !config[field])
if (missing.length > 0) {
throw new Error(dedent`
Missing required config fields:
${missing.map((field) => ` - ${field}`).join('\n')}
`)
}
}
async mergeConfig(filepath, options) {
const databaseConfigDirectory = getDatabaseConfigDirectory(filepath)
const configFilepath = path.join(databaseConfigDirectory, 'config.js')
let userConfig = {}
try {
userConfig = await getConfig(configFilepath)
} catch (error) {}
const config = {
...HyperdbHelper.defaultConfig,
...userConfig,
databaseConfigDirectory
}
config.examples = options.examples
config.functionsFilepath = getFilepathFromConfig(
config,
'functionsFilepath',
'./functions.js'
)
config.schemaFilepath = getFilepathFromConfig(
config,
'schemaFilepath',
'./schema.js'
)
config.configFilepath = getFilepathFromConfig(
config,
'configFilepath',
'./config.js'
)
config.projectPackageJsonFilepath = getFilepathFromConfig(
config,
'projectPackageJsonFilepath',
'./package.json'
)
config.databaseConfigJsonFilepath = path.join(
config.databaseConfigDirectory,
'package.json'
)
config.generatedCodeDirectory = getFilepathFromConfig(
config,
'generatedCodeDirectory',
'./generated'
)
config.generatedPackageJsonFilepath = path.join(
config.generatedCodeDirectory,
'package.json'
)
config.hyperschemaDirectory = path.join(
config.generatedCodeDirectory,
'schemas'
)
config.hyperdbDirectory = path.join(
config.generatedCodeDirectory,
'database'
)
try {
config.package = await getPackageJson(process.cwd())
} catch (error) {}
if (config.package) {
config.moduleType = config.package.type || 'commonjs'
}
return config
}
async getConfig(configFilepath) {
const module = await import(configFilepath)
return { ...module.default }
}
async createDefaultFiles() {
if (await exists(this.config.databaseConfigDirectory)) {
throw new Error(
`Error: Schema directory already exists at ${this.config.databaseConfigDirectory}`
)
}
const exampleIndexFilepath = path.join(
path.dirname(this.config.databaseConfigDirectory),
'index.js'
)
if (this.config.examples) {
if (await exists(exampleIndexFilepath)) {
throw new Error(
`Error: index.js file already exists at ${exampleIndexFilepath}`
)
}
}
await fs.mkdir(this.config.databaseConfigDirectory, {
recursive: true
})
await this.createConfigFile()
await this.createFunctionsFile()
await this.createSchemaFile()
await fs.mkdir(this.config.generatedCodeDirectory)
await this.createDatabaseConfigPackageJsonFile()
await this.createGeneratedPackageJsonFile()
if (this.config.examples) {
await this.createExampleIndexFile(exampleIndexFilepath)
}
}
async createConfigFile() {
await fs.writeFile(
this.config.configFilepath,
templates.configFileTemplate(this.config.moduleType)
)
}
async createFunctionsFile() {
const template = this.config.examples
? examples.functionFileTemplate()
: templates.functionFileTemplate()
await fs.writeFile(this.config.functionsFilepath, template)
}
async createSchemaFile() {
const content = this.config.examples
? examples.schemaFileTemplate()
: templates.schemaFileTemplate()
await fs.writeFile(this.config.schemaFilepath, content)
}
async createDatabaseConfigPackageJsonFile() {
const content = JSON.stringify({
main: './schema.js',
exports: {
'.': './schema.js',
'./config': './config.js'
},
type: 'module'
})
await fs.writeFile(this.config.databaseConfigJsonFilepath, content)
}
async createGeneratedPackageJsonFile() {
const content = JSON.stringify({
main: './database/index.js',
exports: {
'.': './database/index.js',
'./db.json': './database/db.json',
'./messages': './database/messages.js',
'./schemas': './schemas/index.js',
'./schema.json': './schemas/schema.json'
},
type: 'commonjs'
})
await fs.writeFile(this.config.generatedPackageJsonFilepath, content)
}
async createExampleIndexFile(indexFilepath) {
const definitionsFilepath = path.join(
this.config.hyperdbDirectory,
'index.js'
)
const projectDirectory = path.dirname(this.config.databaseConfigDirectory)
const relativePath = path.relative(projectDirectory, definitionsFilepath)
console.log('this.config.moduleType', this.config.moduleType)
const content = examples.exampleIndexFileTemplate({
relativePath,
moduleType: this.config.moduleType
})
await fs.writeFile(indexFilepath, content)
}
checkPackageDependencies() {
if (!this.config.package) {
return this.requiredDependencies
}
const dependencies = this.config.package.dependencies || {}
const toInstall = []
for (const dependency of this.requiredDependencies) {
if (!dependencies[dependency]) {
toInstall.push(dependency)
}
}
return toInstall
}
async getSchemaDefinitions(schemaFilepath) {
const module = await import(schemaFilepath)
return { ...module }
}
async buildSchema(schemaModule) {
const hyperschema = Hyperschema.from(this.config.hyperschemaDirectory)
schemaModule.createSchema(hyperschema)
Hyperschema.toDisk(hyperschema)
const hyperdb = HyperDB.from(
this.config.hyperschemaDirectory,
this.config.hyperdbDirectory
)
schemaModule.createDatabase(hyperdb)
HyperDB.toDisk(hyperdb)
}
}
async function exists(filepath) {
try {
await fs.access(filepath)
return true
} catch (error) {
if (error.code === 'ENOENT') {
return false
}
throw error
}
}
function getDatabaseConfigDirectory(databaseFilepathArgument = './database') {
if (path.isAbsolute(databaseFilepathArgument)) {
return databaseFilepathArgument
}
return path.join(process.cwd(), databaseFilepathArgument)
}
function getFilepathFromConfig(config, configProperty, defaultFilepath) {
const filepath = config[configProperty] || defaultFilepath
if (path.isAbsolute(filepath)) {
return filepath
}
return path.join(config.databaseConfigDirectory, filepath)
}
async function getPackageJson(dir = process.cwd()) {
const packageJsonFilepath = path.join(dir, 'package.json')
if (!(await exists(packageJsonFilepath))) {
return
}
const file = await fs.readFile(packageJsonFilepath, 'utf8')
return JSON.parse(file)
}
async function getConfig(configFilepath) {
const module = await import(configFilepath)
return { ...module.default }
}